Skip to content

Commit 93867f8

Browse files
committed
feat: toast hooks.
1 parent 3d14902 commit 93867f8

File tree

5 files changed

+98
-30
lines changed

5 files changed

+98
-30
lines changed

src/App.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import React from 'react';
22
import { HashRouter as Router } from 'react-router-dom';
33
import Layout from './components/Layout';
44
import AnimatedRoutes from './components/AnimatedRoutes';
5+
import { ToastProvider } from './components/ToastProvider';
56

67
function App() {
78
return (
89
<Router>
9-
<Layout>
10-
<AnimatedRoutes />
11-
</Layout>
10+
<ToastProvider>
11+
<Layout>
12+
<AnimatedRoutes />
13+
</Layout>
14+
</ToastProvider>
1215
</Router>
1316
);
1417
}

src/components/Toast.js

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,34 @@ import { motion } from 'framer-motion';
33

44
import { X } from '@phosphor-icons/react';
55

6-
const Toast = ({ title, message, duration, onClose }) => {
6+
const Toast = ({ id, title, message, duration, removeToast }) => {
77
useEffect(() => {
88
const timer = setTimeout(() => {
9-
onClose();
9+
removeToast(id);
1010
}, duration);
1111

1212
return () => {
1313
clearTimeout(timer);
1414
};
15-
}, [duration, onClose]);
15+
}, [id, duration, removeToast]);
1616

1717
return (
18-
<motion.div
19-
initial={{ y: -100, opacity: 0 }}
20-
animate={{ y: 0, opacity: 1 }}
21-
exit={{ y: -100, opacity: 0 }}
22-
transition={{ type: 'spring', stiffness: 120, damping: 20 }}
23-
className="fixed top-28 -translate-y-1/2 left-0 right-0 mx-auto text-gray-300 py-4 px-10 rounded-lg shadow-lg border backdrop-blur-sm flex items-center justify-between w-96"
24-
style={{ backgroundColor: 'rgba(68, 64, 59, 0.8)', borderColor: '#5a5e64' }}
25-
>
18+
<motion.div
19+
initial={{ x: '100%', opacity: 0 }}
20+
animate={{ x: 0, opacity: 1 }}
21+
exit={{ opacity: 0 }}
22+
transition={{ type: 'spring', stiffness: 120, damping: 20 }}
23+
className="text-gray-300 py-4 px-10 rounded-lg shadow-lg border backdrop-blur-sm flex items-center justify-between w-96 mb-4"
24+
style={{ backgroundColor: 'rgba(68, 64, 59, 0.8)', borderColor: '#5a5e64' }}
25+
>
2626
<div className="flex flex-col text-sm">
2727
<span>{title}</span>
2828
<span>{message}</span>
2929
</div>
30-
<button onClick={onClose} className="pr-2">
30+
<button onClick={() => removeToast(id)} className="pr-2">
3131
<X size={24} weight="bold" />
3232
</button>
3333
</motion.div>
3434
);
3535
};
36-
3736
export default Toast;

src/components/ToastProvider.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
2+
import React, { createContext, useState, useCallback } from 'react';
3+
import Toast from './Toast';
4+
5+
export const ToastContext = createContext();
6+
7+
let id = 0;
8+
9+
export const ToastProvider = ({ children }) => {
10+
const [toasts, setToasts] = useState([]);
11+
12+
const addToast = useCallback((toast) => {
13+
const newToast = { ...toast, id: id++ };
14+
setToasts((prevToasts) => {
15+
if (prevToasts.length >= 5) {
16+
const updatedToasts = prevToasts.slice(0, prevToasts.length - 1);
17+
return [newToast, ...updatedToasts];
18+
}
19+
return [newToast, ...prevToasts];
20+
});
21+
}, []);
22+
23+
const removeToast = useCallback((id) => {
24+
setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id));
25+
}, []);
26+
27+
28+
return (
29+
<ToastContext.Provider value={{ addToast, removeToast }}>
30+
{children}
31+
<div className="fixed top-28 right-10 z-50">
32+
{toasts.map((toast) => (
33+
<Toast
34+
key={toast.id}
35+
id={toast.id}
36+
title={toast.title}
37+
message={toast.message}
38+
duration={toast.duration}
39+
removeToast={removeToast}
40+
/>
41+
))}
42+
</div>
43+
</ToastContext.Provider>
44+
);
45+
};

src/hooks/useToast.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
import { useContext } from 'react';
3+
import { ToastContext } from '../components/ToastProvider';
4+
5+
export const useToast = () => {
6+
return useContext(ToastContext);
7+
};

src/pages/BlogPostPage.js

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
55
import { customTheme } from '../utils/customTheme';
66
import PostMetadata from '../components/PostMetadata';
77
import CodeModal from '../components/CodeModal';
8-
import Toast from '../components/Toast';
8+
import { useToast } from '../hooks/useToast';
99
import { AnimatePresence } from 'framer-motion';
1010
import { ArrowLeftIcon, ArrowSquareOutIcon, ClipboardIcon, ArrowsOutSimpleIcon } from '@phosphor-icons/react';
1111

@@ -18,11 +18,33 @@ const LinkRenderer = ({ href, children }) => {
1818
);
1919
};
2020

21-
const CodeBlock = ({ node, inline, className, children, openModal, showToast, ...props }) => {
21+
const CodeBlock = ({ node, inline, className, children, openModal, ...props }) => {
2222
const match = /language-(\w+)/.exec(className || '');
23+
const { addToast } = useToast();
2324
const handleCopy = () => {
24-
navigator.clipboard.writeText(String(children));
25-
showToast('Success', 'Copied to clipboard!');
25+
const textToCopy = String(children);
26+
if (navigator.clipboard && navigator.clipboard.writeText) {
27+
navigator.clipboard.writeText(textToCopy).then(() => {
28+
addToast({ title: 'Success', message: 'Copied to clipboard!', duration: 3000 });
29+
}, () => {
30+
addToast({ title: 'Error', message: 'Failed to copy!', duration: 3000 });
31+
});
32+
} else {
33+
const textArea = document.createElement('textarea');
34+
textArea.value = textToCopy;
35+
textArea.style.position = 'fixed';
36+
textArea.style.left = '-9999px';
37+
document.body.appendChild(textArea);
38+
textArea.focus();
39+
textArea.select();
40+
try {
41+
document.execCommand('copy');
42+
addToast({ title: 'Success', message: 'Copied to clipboard!', duration: 3000 });
43+
} catch (err) {
44+
addToast({ title: 'Error', message: 'Failed to copy!', duration: 3000 });
45+
}
46+
document.body.removeChild(textArea);
47+
}
2648
};
2749

2850
return !inline && match ? (
@@ -61,7 +83,6 @@ const BlogPostPage = () => {
6183
const contentRef = useRef(null);
6284
const [isModalOpen, setIsModalOpen] = useState(false);
6385
const [modalContent, setModalContent] = useState('');
64-
const [toastMessage, setToastMessage] = useState('');
6586

6687
const openModal = (content) => {
6788
setModalContent(content);
@@ -73,10 +94,6 @@ const BlogPostPage = () => {
7394
setModalContent('');
7495
};
7596

76-
const showToast = (title, message) => {
77-
setToastMessage({ title, message });
78-
};
79-
8097
useEffect(() => {
8198
const fetchPost = async () => {
8299
setLoading(true);
@@ -177,7 +194,7 @@ const BlogPostPage = () => {
177194
<ArrowLeftIcon size={24} /> Back to Home
178195
</Link>
179196
<div ref={contentRef} className="prose prose-xl prose-dark max-w-none">
180-
<ReactMarkdown components={{ a: LinkRenderer, code: (props) => <CodeBlock {...props} openModal={openModal} showToast={showToast} /> }}>{post.body}</ReactMarkdown>
197+
<ReactMarkdown components={{ a: LinkRenderer, code: (props) => <CodeBlock {...props} openModal={openModal} /> }}>{post.body}</ReactMarkdown>
181198
</div>
182199
</div>
183200
<div className="hidden lg:block">
@@ -188,11 +205,8 @@ const BlogPostPage = () => {
188205
<CodeModal isOpen={isModalOpen} onClose={closeModal}>
189206
{modalContent}
190207
</CodeModal>
191-
<AnimatePresence>
192-
{toastMessage && <Toast title={toastMessage.title} message={toastMessage.message} duration={3000} onClose={() => setToastMessage(null)} />}
193-
</AnimatePresence>
194208
</div>
195209
);
196210
};
197211

198-
export default BlogPostPage;
212+
export default BlogPostPage;

0 commit comments

Comments
 (0)