Skip to content

Commit 597684e

Browse files
committed
feat: toast
1 parent 0e81a0f commit 597684e

File tree

3 files changed

+142
-15
lines changed

3 files changed

+142
-15
lines changed

src/components/CodeModal.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { useEffect } from 'react';
2+
import { X } from '@phosphor-icons/react';
3+
import { motion, AnimatePresence } from 'framer-motion';
4+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5+
import { customTheme } from '../utils/customTheme';
6+
7+
const CodeModal = ({ isOpen, onClose, children }) => {
8+
useEffect(() => {
9+
if (isOpen) {
10+
document.body.style.overflow = 'hidden';
11+
} else {
12+
document.body.style.overflow = 'unset';
13+
}
14+
return () => {
15+
document.body.style.overflow = 'unset';
16+
};
17+
}, [isOpen]);
18+
19+
return (
20+
<AnimatePresence>
21+
{isOpen && (
22+
<motion.div
23+
className="fixed inset-0 bg-black bg-opacity-75 flex justify-center items-center z-50 p-4"
24+
onClick={onClose}
25+
initial={{ opacity: 0 }}
26+
animate={{ opacity: 1 }}
27+
exit={{ opacity: 0 }}
28+
>
29+
<motion.div
30+
className="relative bg-gray-800 rounded-lg shadow-lg p-6 w-3/4 h-3/4"
31+
onClick={e => e.stopPropagation()}
32+
initial={{ scale: 0.8, opacity: 0 }}
33+
animate={{ scale: 1, opacity: 1 }}
34+
exit={{ scale: 0.8, opacity: 0 }}
35+
transition={{ duration: 0.2 }}
36+
>
37+
<button
38+
onClick={onClose}
39+
className="absolute top-2 right-2 text-white text-2xl bg-gray-800 rounded-full p-2 hover:bg-gray-700 focus:outline-none"
40+
>
41+
<X size={24} weight="bold" />
42+
</button>
43+
<SyntaxHighlighter
44+
style={customTheme}
45+
language="jsx"
46+
PreTag="pre"
47+
className="overflow-auto h-full"
48+
codeTagProps={{ style: { fontFamily: "'JetBrains Mono', monospace" } }}
49+
>
50+
{children}
51+
</SyntaxHighlighter>
52+
</motion.div>
53+
</motion.div>
54+
)}
55+
</AnimatePresence>
56+
);
57+
};
58+
59+
export default CodeModal;

src/components/Toast.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React, { useState, useEffect } from 'react';
2+
3+
import { X } from '@phosphor-icons/react';
4+
5+
const Toast = ({ title, message, duration, onClose }) => {
6+
useEffect(() => {
7+
const timer = setTimeout(() => {
8+
onClose();
9+
}, duration);
10+
11+
return () => {
12+
clearTimeout(timer);
13+
};
14+
}, [duration, onClose]);
15+
16+
return (
17+
<div
18+
className="fixed top-28 left-1/2 -translate-x-1/2 text-gray-300 py-4 px-10 rounded-lg shadow-lg border backdrop-blur-sm flex items-center justify-between w-96"
19+
style={{ backgroundColor: 'rgba(68, 64, 59, 0.65)', borderColor: '#5a5e64' }}
20+
>
21+
<div className="flex flex-col text-sm">
22+
<span>{title}</span>
23+
<span>{message}</span>
24+
</div>
25+
<button onClick={onClose} className="pr-2">
26+
<X size={24} weight="bold" />
27+
</button>
28+
</div>
29+
);
30+
};
31+
32+
export default Toast;

src/pages/BlogPostPage.js

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import ReactMarkdown from 'react-markdown';
44
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
55
import { customTheme } from '../utils/customTheme';
66
import PostMetadata from '../components/PostMetadata';
7-
import { ArrowLeftIcon, ArrowSquareOutIcon } from '@phosphor-icons/react';
7+
import CodeModal from '../components/CodeModal';
8+
import Toast from '../components/Toast';
9+
import { ArrowLeftIcon, ArrowSquareOutIcon, ClipboardIcon, ArrowsOutSimpleIcon } from '@phosphor-icons/react';
810

911
const LinkRenderer = ({ href, children }) => {
1012
const isExternal = href.startsWith('http') || href.startsWith('https');
@@ -15,22 +17,35 @@ const LinkRenderer = ({ href, children }) => {
1517
);
1618
};
1719

18-
19-
20-
const CodeBlock = ({ node, inline, className, children, ...props }) => {
20+
const CodeBlock = ({ node, inline, className, children, openModal, showToast, ...props }) => {
2121
const match = /language-(\w+)/.exec(className || '');
22+
const handleCopy = () => {
23+
navigator.clipboard.writeText(String(children));
24+
showToast('Success', 'Copied to clipboard!');
25+
};
26+
2227
return !inline && match ? (
23-
<SyntaxHighlighter
24-
style={customTheme}
25-
language={match[1]}
26-
PreTag="div"
27-
{...props}
28-
codeTagProps={{ style: { fontFamily: "'JetBrains Mono', monospace" } }}
29-
>
30-
{String(children).replace(/\n$/, '')}
31-
</SyntaxHighlighter>
28+
<div className="relative">
29+
<div className="absolute top-2 right-2 flex gap-2">
30+
<button onClick={() => openModal(String(children).replace(/\n$/, ''))} className="text-white bg-gray-700 p-1 rounded opacity-75 hover:opacity-100">
31+
<ArrowsOutSimpleIcon size={20} />
32+
</button>
33+
<button onClick={handleCopy} className="text-white bg-gray-700 p-1 rounded opacity-75 hover:opacity-100">
34+
<ClipboardIcon size={20} />
35+
</button>
36+
</div>
37+
<SyntaxHighlighter
38+
style={customTheme}
39+
language={match[1]}
40+
PreTag="div"
41+
{...props}
42+
codeTagProps={{ style: { fontFamily: "'JetBrains Mono', monospace" } }}
43+
>
44+
{String(children).replace(/\n$/, '')}
45+
</SyntaxHighlighter>
46+
</div>
3247
) : (
33-
<code className={`${className} font-mono bg-gray-800`} {...props}>
48+
<code className={`${className} font-mono`} {...props}>
3449
{children}
3550
</code>
3651
);
@@ -43,6 +58,23 @@ const BlogPostPage = () => {
4358
const [readingProgress, setReadingProgress] = useState(0);
4459
const [isAtTop, setIsAtTop] = useState(true); // New state for tracking if at top
4560
const contentRef = useRef(null);
61+
const [isModalOpen, setIsModalOpen] = useState(false);
62+
const [modalContent, setModalContent] = useState('');
63+
const [toastMessage, setToastMessage] = useState('');
64+
65+
const openModal = (content) => {
66+
setModalContent(content);
67+
setIsModalOpen(true);
68+
};
69+
70+
const closeModal = () => {
71+
setIsModalOpen(false);
72+
setModalContent('');
73+
};
74+
75+
const showToast = (title, message) => {
76+
setToastMessage({ title, message });
77+
};
4678

4779
useEffect(() => {
4880
const fetchPost = async () => {
@@ -144,14 +176,18 @@ const BlogPostPage = () => {
144176
<ArrowLeftIcon size={24} /> Back to Home
145177
</Link>
146178
<div ref={contentRef} className="prose prose-xl prose-dark max-w-none">
147-
<ReactMarkdown components={{ a: LinkRenderer, code: CodeBlock }}>{post.body}</ReactMarkdown>
179+
<ReactMarkdown components={{ a: LinkRenderer, code: (props) => <CodeBlock {...props} openModal={openModal} showToast={showToast} /> }}>{post.body}</ReactMarkdown>
148180
</div>
149181
</div>
150182
<div className="hidden lg:block">
151183
<PostMetadata metadata={post.attributes} readingProgress={readingProgress} isAtTop={isAtTop} overrideDate={post.attributes.date} updatedDate={post.attributes.updated} />
152184
</div>
153185
</div>
154186
</div>
187+
<CodeModal isOpen={isModalOpen} onClose={closeModal}>
188+
{modalContent}
189+
</CodeModal>
190+
{toastMessage && <Toast title={toastMessage.title} message={toastMessage.message} duration={3000} onClose={() => setToastMessage(null)} />}
155191
</div>
156192
);
157193
};

0 commit comments

Comments
 (0)