Skip to content

Commit 8eb9c1a

Browse files
committed
feat: Enhance CodeBlock component with preview functionality and refactor code structure
1 parent 9ae3c2a commit 8eb9c1a

File tree

1 file changed

+120
-110
lines changed

1 file changed

+120
-110
lines changed
Lines changed: 120 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { programmingLanguages } from "@/utils/langauge-extension"
2-
import { Tooltip, Modal, ConfigProvider, Button } from "antd"
2+
import { Tooltip } from "antd"
33
import {
44
CopyCheckIcon,
55
CopyIcon,
66
DownloadIcon,
7-
InfoIcon,
8-
ExternalLinkIcon
7+
EyeIcon,
8+
CodeIcon
99
} from "lucide-react"
10-
import { FC, useState, useRef } from "react"
10+
import { FC, useState, useRef, useEffect } from "react"
1111
import { useTranslation } from "react-i18next"
1212
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
1313
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"
@@ -20,9 +20,34 @@ interface Props {
2020

2121
export const CodeBlock: FC<Props> = ({ language, value }) => {
2222
const [isBtnPressed, setIsBtnPressed] = useState(false)
23-
const [previewVisible, setPreviewVisible] = useState(false)
23+
const computeKey = () => {
24+
const base = `${language}::${value?.slice(0, 200)}`
25+
let hash = 0
26+
for (let i = 0; i < base.length; i++) {
27+
hash = (hash * 31 + base.charCodeAt(i)) >>> 0
28+
}
29+
return hash.toString(36)
30+
}
31+
const keyRef = useRef<string>(computeKey())
32+
const mapRef = useRef<Map<string, boolean> | null>(null)
33+
if (!mapRef.current) {
34+
if (typeof window !== "undefined") {
35+
// @ts-ignore
36+
if (!window.__codeBlockPreviewState) {
37+
// @ts-ignore
38+
window.__codeBlockPreviewState = new Map()
39+
}
40+
// @ts-ignore
41+
mapRef.current = window.__codeBlockPreviewState as Map<string, boolean>
42+
} else {
43+
mapRef.current = new Map()
44+
}
45+
}
46+
const globalStateMap = mapRef.current!
47+
const [showPreview, setShowPreview] = useState<boolean>(
48+
() => globalStateMap.get(keyRef.current) || false
49+
)
2450
const { t } = useTranslation("common")
25-
const iframeRef = useRef<HTMLIFrameElement>(null)
2651

2752
const handleCopy = () => {
2853
navigator.clipboard.writeText(value)
@@ -32,8 +57,20 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
3257
}, 4000)
3358
}
3459

35-
const handlePreviewClose = () => {
36-
setPreviewVisible(false)
60+
const isPreviewable = ["html", "svg", "xml"].includes(
61+
(language || "").toLowerCase()
62+
)
63+
64+
const buildPreviewDoc = () => {
65+
const code = value || ""
66+
if ((language || "").toLowerCase() === "svg") {
67+
const hasSvgTag = /<svg[\s>]/i.test(code)
68+
const svgMarkup = hasSvgTag
69+
? code
70+
: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>${code}</svg>`
71+
return `<!doctype html><html><head><meta charset='utf-8'/><style>html,body{margin:0;padding:0;display:flex;align-items:center;justify-content:center;background:#fff;height:100%;}</style></head><body>${svgMarkup}</body></html>`
72+
}
73+
return `<!doctype html><html><head><meta charset='utf-8'/></head><body>${code}</body></html>`
3774
}
3875

3976
const handleDownload = () => {
@@ -48,30 +85,59 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
4885
window.URL.revokeObjectURL(url)
4986
}
5087

51-
const handleOpenInNewTab = () => {
52-
const blob = new Blob([value], { type: "text/html" })
53-
const url = URL.createObjectURL(blob)
54-
window.open(url, "_blank")
55-
}
88+
useEffect(() => {
89+
globalStateMap.set(keyRef.current, showPreview)
90+
}, [showPreview])
91+
92+
useEffect(() => {
93+
const newKey = computeKey()
94+
if (newKey !== keyRef.current) {
95+
keyRef.current = newKey
96+
if (globalStateMap.has(newKey)) {
97+
const prev = globalStateMap.get(newKey)!
98+
if (prev !== showPreview) setShowPreview(prev)
99+
}
100+
}
101+
}, [language, value])
102+
103+
useEffect(() => {
104+
if (!isPreviewable && showPreview) setShowPreview(false)
105+
}, [isPreviewable])
56106

57107
return (
58108
<>
59109
<div className="not-prose">
60110
<div className=" [&_div+div]:!mt-0 my-4 bg-zinc-950 rounded-xl">
61-
<div className="flex flex-row px-4 py-2 rounded-t-xl bg-gray-800 ">
111+
<div className="flex flex-row px-4 py-2 rounded-t-xl gap-3 bg-gray-800 ">
112+
{isPreviewable && (
113+
<div className="flex rounded-md overflow-hidden border border-gray-700">
114+
<button
115+
onClick={() => setShowPreview(false)}
116+
className={`px-2 flex items-center gap-1 text-xs transition-colors ${
117+
!showPreview
118+
? "bg-gray-700 text-white"
119+
: "bg-transparent text-gray-300 hover:bg-gray-700/60"
120+
}`}
121+
aria-label={t("showCode") || "Code"}>
122+
<CodeIcon className="size-3" />
123+
</button>
124+
<button
125+
onClick={() => setShowPreview(true)}
126+
className={`px-2 flex items-center gap-1 text-xs transition-colors ${
127+
showPreview
128+
? "bg-gray-700 text-white"
129+
: "bg-transparent text-gray-300 hover:bg-gray-700/60"
130+
}`}
131+
aria-label={t("preview") || "Preview"}>
132+
<EyeIcon className="size-3" />
133+
</button>
134+
</div>
135+
)}
136+
62137
<span className="font-mono text-xs">{language || "text"}</span>
63138
</div>
64139
<div className="sticky top-9 md:top-[5.75rem]">
65-
<div className="absolute bottom-0 right-2 flex h-9 items-center">
66-
{/* {language === "html" && (
67-
<Tooltip title={t("preview")}>
68-
<button
69-
onClick={() => setPreviewVisible(true)}
70-
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-200 hover:bg-gray-700 hover:text-gray-100 focus:outline-none">
71-
<InfoIcon className="size-4" />
72-
</button>
73-
</Tooltip>
74-
)} */}
140+
<div className="absolute bottom-0 right-2 flex h-9 items-center gap-1">
75141
<Tooltip title={t("downloadCode")}>
76142
<button
77143
onClick={handleDownload}
@@ -93,97 +159,41 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
93159
</div>
94160
</div>
95161

96-
<SyntaxHighlighter
97-
language={language}
98-
style={coldarkDark}
99-
PreTag="div"
100-
customStyle={{
101-
margin: 0,
102-
width: "100%",
103-
background: "transparent",
104-
padding: "1.5rem 1rem"
105-
}}
106-
lineNumberStyle={{
107-
userSelect: "none"
108-
}}
109-
codeTagProps={{
110-
style: {
111-
fontSize: "0.9rem",
112-
fontFamily: "var(--font-mono)"
113-
}
114-
}}>
115-
{value}
116-
</SyntaxHighlighter>
117-
</div>
118-
</div>
119-
{previewVisible && (
120-
<ConfigProvider
121-
theme={{
122-
components: {
123-
Modal: {
124-
contentBg: "#1e1e1e",
125-
headerBg: "#1e1e1e",
126-
titleColor: "#ffffff"
127-
}
128-
}
129-
}}>
130-
<Modal
131-
title={
132-
<div className="flex items-center text-white">
133-
<InfoIcon className="mr-2 size-5" />
134-
<span>HTML Preview</span>
135-
</div>
136-
}
137-
open={previewVisible}
138-
onCancel={handlePreviewClose}
139-
footer={
140-
<div className="flex justify-end gap-2">
141-
<Button
142-
icon={<ExternalLinkIcon className="size-4" />}
143-
onClick={handleOpenInNewTab}>
144-
Open in new tab
145-
</Button>
146-
147-
<Button
148-
icon={<DownloadIcon className="size-4" />}
149-
onClick={handleDownload}>
150-
{t("downloadCode")}
151-
</Button>
152-
</div>
153-
}
154-
width={"80%"}
155-
zIndex={999999}
156-
centered
157-
styles={{
158-
body: {
159-
padding: 0,
160-
backgroundColor: "#f5f5f5",
161-
borderRadius: "0 0 8px 8px"
162-
},
163-
header: {
164-
borderBottom: "1px solid #333",
165-
padding: "12px 24px"
166-
},
167-
mask: {
168-
backdropFilter: "blur(4px)",
169-
backgroundColor: "rgba(0, 0, 0, 0.6)"
170-
},
171-
content: {
172-
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)"
173-
}
174-
}}>
175-
<div className={`relative w-full h-[70vh] bg-white`}>
162+
{!showPreview && (
163+
<SyntaxHighlighter
164+
language={language}
165+
style={coldarkDark}
166+
PreTag="div"
167+
customStyle={{
168+
margin: 0,
169+
width: "100%",
170+
background: "transparent",
171+
padding: "1.5rem 1rem"
172+
}}
173+
lineNumberStyle={{
174+
userSelect: "none"
175+
}}
176+
codeTagProps={{
177+
style: {
178+
fontSize: "0.9rem",
179+
fontFamily: "var(--font-mono)"
180+
}
181+
}}>
182+
{value}
183+
</SyntaxHighlighter>
184+
)}
185+
{showPreview && isPreviewable && (
186+
<div className="w-full h-[420px] bg-white rounded-b-xl overflow-hidden border-t border-gray-800">
176187
<iframe
177-
ref={iframeRef}
178-
srcDoc={value}
179-
title="HTML Preview"
188+
title="Preview"
189+
srcDoc={buildPreviewDoc()}
180190
className="w-full h-full border-0"
181191
sandbox="allow-scripts allow-same-origin"
182192
/>
183193
</div>
184-
</Modal>
185-
</ConfigProvider>
186-
)}
194+
)}
195+
</div>
196+
</div>
187197
</>
188198
)
189199
}

0 commit comments

Comments
 (0)