Skip to content

Commit c667f8f

Browse files
committed
feat: add copy code button with clipboard support to code viewer panel
1 parent 1aa683e commit c667f8f

3 files changed

Lines changed: 131 additions & 1 deletion

File tree

static/script.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,4 +416,69 @@ if (isDetailPage) {
416416
if (evt.key === "Escape") closeCodePanel();
417417
});
418418

419+
// ----------------------------------------------------------
420+
// Copy Code button
421+
// ----------------------------------------------------------
422+
var btnCopyCode = document.getElementById("btn-copy-code");
423+
var copyToast = document.getElementById("copy-toast");
424+
var toastTimeout = null;
425+
426+
function showCopySuccess() {
427+
if (!btnCopyCode) return;
428+
429+
// Swap icons on the button
430+
var copyIcon = btnCopyCode.querySelector(".copy-icon");
431+
var checkIcon = btnCopyCode.querySelector(".check-icon");
432+
var btnLabel = btnCopyCode.querySelector(".copy-btn-label");
433+
434+
if (copyIcon) copyIcon.style.display = "none";
435+
if (checkIcon) checkIcon.style.display = "inline";
436+
if (btnLabel) btnLabel.textContent = "Copied!";
437+
btnCopyCode.classList.add("copied");
438+
btnCopyCode.disabled = true;
439+
440+
// Show toast
441+
if (copyToast) {
442+
copyToast.classList.add("show");
443+
}
444+
445+
// Auto-reset after 2.5 s
446+
clearTimeout(toastTimeout);
447+
toastTimeout = setTimeout(function () {
448+
if (copyIcon) copyIcon.style.display = "inline";
449+
if (checkIcon) checkIcon.style.display = "none";
450+
if (btnLabel) btnLabel.textContent = "Copy Code";
451+
btnCopyCode.classList.remove("copied");
452+
btnCopyCode.disabled = false;
453+
if (copyToast) copyToast.classList.remove("show");
454+
}, 2500);
455+
}
456+
457+
if (btnCopyCode) {
458+
btnCopyCode.addEventListener("click", function () {
459+
var code = codeContentEl ? codeContentEl.textContent : "";
460+
if (!code || code === "Loading..." || code === "Loading starter code...") return;
461+
462+
// Use Clipboard API with textarea fallback
463+
if (navigator.clipboard && navigator.clipboard.writeText) {
464+
navigator.clipboard.writeText(code).then(showCopySuccess).catch(function () {
465+
fallbackCopy(code);
466+
});
467+
} else {
468+
fallbackCopy(code);
469+
}
470+
});
471+
}
472+
473+
function fallbackCopy(text) {
474+
var ta = document.createElement("textarea");
475+
ta.value = text;
476+
ta.style.cssText = "position:fixed;top:-9999px;left:-9999px;opacity:0";
477+
document.body.appendChild(ta);
478+
ta.focus();
479+
ta.select();
480+
try { document.execCommand("copy"); showCopySuccess(); } catch (e) { /* silent fail */ }
481+
document.body.removeChild(ta);
482+
}
483+
419484
} // end isDetailPage

static/style.css

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,60 @@ select:focus {
14401440
transition: background var(--t);
14411441
}
14421442
.code-panel-close:hover { background: rgba(255,255,255,0.13); }
1443+
1444+
/* Copy Code button */
1445+
.btn-copy-code {
1446+
display: inline-flex;
1447+
align-items: center;
1448+
gap: 6px;
1449+
background: rgba(79, 110, 247, 0.15);
1450+
border: 1px solid rgba(79, 110, 247, 0.35);
1451+
border-radius: var(--r-xs);
1452+
padding: 6px 14px;
1453+
font-size: 0.82rem;
1454+
font-weight: 600;
1455+
color: var(--indigo-500);
1456+
cursor: pointer;
1457+
transition: background var(--t), border-color var(--t), color var(--t), transform 0.1s ease;
1458+
}
1459+
.btn-copy-code:hover {
1460+
background: rgba(79, 110, 247, 0.28);
1461+
border-color: var(--indigo-500);
1462+
color: #a5b4fc;
1463+
}
1464+
.btn-copy-code:active { transform: scale(0.97); }
1465+
.btn-copy-code.copied {
1466+
background: rgba(16, 185, 129, 0.18);
1467+
border-color: rgba(16, 185, 129, 0.4);
1468+
color: #6ee7b7;
1469+
}
1470+
1471+
/* Copy success toast */
1472+
.copy-toast {
1473+
position: fixed;
1474+
bottom: 28px;
1475+
left: 50%;
1476+
transform: translateX(-50%) translateY(20px);
1477+
display: inline-flex;
1478+
align-items: center;
1479+
gap: 8px;
1480+
background: #10b981;
1481+
color: #fff;
1482+
font-size: 0.88rem;
1483+
font-weight: 600;
1484+
padding: 10px 20px;
1485+
border-radius: var(--r-full);
1486+
box-shadow: 0 6px 24px rgba(16, 185, 129, 0.45);
1487+
z-index: 500;
1488+
opacity: 0;
1489+
pointer-events: none;
1490+
transition: opacity 0.25s ease, transform 0.25s ease;
1491+
}
1492+
.copy-toast.show {
1493+
opacity: 1;
1494+
transform: translateX(-50%) translateY(0);
1495+
}
1496+
14431497
.code-viewer {
14441498
flex: 1; overflow: auto;
14451499
padding: 24px 28px;

templates/project.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ <h3 class="sidebar-card-title">
221221
</div>
222222
<div class="code-panel-actions">
223223
<a href="/project/{{ project.id }}/download" class="code-panel-download">Download file</a>
224+
<button class="btn-copy-code" id="btn-copy-code" aria-label="Copy code to clipboard">
225+
<svg class="copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
226+
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display:none"><polyline points="20 6 9 17 4 12"/></svg>
227+
<span class="copy-btn-label">Copy Code</span>
228+
</button>
224229
<button class="code-panel-close" id="code-panel-close">Close</button>
225230
</div>
226231
</div>
@@ -230,6 +235,12 @@ <h3 class="sidebar-card-title">
230235
<!-- Dark overlay behind the code panel -->
231236
<div class="code-panel-overlay" id="code-panel-overlay"></div>
232237

238+
<!-- Copy success toast notification -->
239+
<div class="copy-toast" id="copy-toast" role="status" aria-live="polite">
240+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
241+
Copied to clipboard!
242+
</div>
243+
233244
<!-- ============================================================
234245
Footer
235246
============================================================ -->
@@ -248,7 +259,7 @@ <h3 class="sidebar-card-title">
248259

249260
<!-- Pass project ID to the JavaScript without hardcoding -->
250261
<script>
251-
var PROJECT_ID = {{ project.id }};
262+
var PROJECT_ID = parseInt("{{ project.id }}", 10);
252263
</script>
253264
<script src="/static/script.js"></script>
254265
</body>

0 commit comments

Comments
 (0)