Skip to content

Commit 593340e

Browse files
authored
feat:add-copybutton (#103)
1 parent 29683e4 commit 593340e

2 files changed

Lines changed: 87 additions & 11 deletions

File tree

UI/frontend/src/App.css

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,35 @@ body {
217217
font-size: 1.4rem;
218218
}
219219
}
220+
pre {
221+
position: relative;
222+
background-color: #1e1e1e;
223+
color: #f8f8f2;
224+
padding: 1rem;
225+
border-radius: 8px;
226+
overflow-x: auto;
227+
font-family: 'Fira Code', monospace;
228+
}
229+
230+
/* --- Copy Button --- */
231+
.copy-btn {
232+
position: absolute;
233+
top: 8px;
234+
right: 8px;
235+
transform: translateY(-50%);
236+
background-color: #2d2d2d;
237+
color: #ffffff;
238+
border: none;
239+
border-radius: 4px;
240+
font-size: 12px;
241+
padding: 4px 8px;
242+
cursor: pointer;
243+
opacity: 0.7;
244+
transition: opacity 0.2s, background-color 0.2s;
245+
}
246+
247+
.copy-btn:hover {
248+
opacity: 1;
249+
background-color: #3e3e3e;
250+
}
251+

UI/frontend/src/App.jsx

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,64 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
33
import LessonPage from './pages/LessonPage';
44
import { getAllLessons } from './utils/markdownloader';
55
import './App.css';
66

77
const App = () => {
8-
// Get all lessons to find the first one for the default redirect
98
const lessons = getAllLessons();
109
const firstLesson = lessons.length > 0 ? lessons[0] : null;
1110

11+
useEffect(() => {
12+
// Function to add copy buttons to all <pre><code> blocks
13+
const addCopyButtons = (root = document) => {
14+
const codeBlocks = root.querySelectorAll("pre");
15+
16+
codeBlocks.forEach((block) => {
17+
const code = block.querySelector(":scope > code"); // only direct child <code>
18+
if (!code) return; // skip outer <pre> that doesn't directly contain code
19+
if (block.querySelector(".copy-btn")) return; // avoid duplicates
20+
21+
22+
block.style.position = "relative";
23+
24+
const button = document.createElement("button");
25+
button.className =
26+
"copy-btn absolute top-2 right-2 bg-gray-800 text-white text-xs px-2 py-1 rounded";
27+
button.innerText = "Copy";
28+
29+
button.addEventListener("click", async () => {
30+
const text = code.innerText || "";
31+
await navigator.clipboard.writeText(text);
32+
button.innerText = "Copied!";
33+
setTimeout(() => (button.innerText = "Copy"), 2000);
34+
});
35+
36+
block.appendChild(button);
37+
});
38+
};
39+
40+
// Initial run
41+
addCopyButtons();
42+
43+
// Watch for dynamically loaded lessons
44+
const observer = new MutationObserver((mutations) => {
45+
for (const mutation of mutations) {
46+
mutation.addedNodes.forEach((node) => {
47+
if (node.nodeType === 1) addCopyButtons(node);
48+
});
49+
}
50+
});
51+
52+
observer.observe(document.body, { childList: true, subtree: true });
53+
return () => observer.disconnect();
54+
}, []);
55+
1256
return (
1357
<BrowserRouter>
1458
<Routes>
1559
{/* Redirect root to first lesson */}
16-
<Route
17-
path="/"
60+
<Route
61+
path="/"
1862
element={
1963
firstLesson ? (
2064
<Navigate to={`/lessons/${firstLesson.slug}`} replace />
@@ -24,15 +68,15 @@ const App = () => {
2468
<p>Please add markdown files to the content folder.</p>
2569
</div>
2670
)
27-
}
71+
}
2872
/>
29-
73+
3074
{/* Dynamic lesson route */}
3175
<Route path="/lessons/:slug" element={<LessonPage />} />
32-
76+
3377
{/* 404 fallback */}
34-
<Route
35-
path="*"
78+
<Route
79+
path="*"
3680
element={
3781
<div style={{ padding: '2rem', textAlign: 'center' }}>
3882
<h1>404 - Page Not Found</h1>
@@ -43,11 +87,11 @@ const App = () => {
4387
</a>
4488
)}
4589
</div>
46-
}
90+
}
4791
/>
4892
</Routes>
4993
</BrowserRouter>
5094
);
5195
};
5296

53-
export default App;
97+
export default App;

0 commit comments

Comments
 (0)