feat: add Eurda console feature with pause/play execution and improve UX

- implement pause/play execution
- add scroll synch for textarea and pre elements
- optimze css transition duration for smoother animations
This commit is contained in:
Asad 2025-01-18 15:34:54 -05:00
parent 8a2b45099a
commit 52f6af96f7
15 changed files with 357 additions and 137 deletions

@ -5,17 +5,14 @@ const xss = require('xss');
const validateAuth = (req, res, next) => {
const { email, password } = req.body;
// email validation
if (!email || !validator.isEmail(email)) {
return res.status(400).json({ error: 'Invalid email format' });
}
// password validation
if (!password || password.length < 4 || password.length > 100) {
return res.status(400).json({ error: 'Invalid password format' });
}
// sanitize email
req.body.email = validator.normalizeEmail(email.toLowerCase().trim());
next();
@ -25,12 +22,10 @@ const validateAuth = (req, res, next) => {
const validateSnippet = (req, res, next) => {
const { title } = req.body;
// title is required
if (!title || typeof title !== 'string' || title.length > 200) {
return res.status(400).json({ error: 'Invalid title' });
}
// sanitize title against XSS
req.body.title = xss(title.trim());
// enforce size limits on code content

@ -5,42 +5,81 @@ Prism.manual = true;
createApp({
data() {
return {
token: localStorage.getItem('token'),
token: localStorage.getItem("token"),
showLogin: false,
showShareModal: false,
loginEmail: '',
loginPassword: '',
title: '',
html: '',
css: '',
js: '',
shareUrl: '',
loginEmail: "",
loginPassword: "",
title: "",
html: `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Bin Code</title>
</head>
<body>
</body>
</html>
`,
css: "",
js: "",
shareUrl: "",
currentShareId: null,
isDragging: false,
startX: null,
startWidth: null,
containerWidth: null,
editorWidth: '50%',
editorWidth: "50%",
minWidth: 250,
maxWidth: null,
// Tab state
activeTab: 'html',
activeTab: "html",
tabs: [
{ id: 'html', label: 'HTML', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#E65100" d="m4 4 2 22 10 2 10-2 2-22Zm19.72 7H11.28l.29 3h11.86l-.802 9.335L15.99 25l-6.635-1.646L8.93 19h3.02l.19 2 3.86.77 3.84-.77.29-4H8.84L8 8h16Z"/></svg>', language: 'markup' },
{ id: 'css', label: 'CSS', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#42a5f5" d="m29.18 4-3.57 18.36-.33 1.64-4.74 1.57-3.28 1.09L13.21 28 2.87 24.05 4.05 18h4.2l-.44 2.85 6.34 2.42.78-.26 6.52-2.16.17-.83.79-4.02H4.44l.74-3.76.05-.24h17.96l.78-4H6l.78-4z"/></svg>', language: 'css' },
{ id: 'js', label: 'JavaScript', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#ffca28" d="M2 2h12v12H2zm3.153 10.027c.267.567.794 1.033 1.694 1.033 1 0 1.686-.533 1.686-1.7V7.507H7.4v3.827c0 .573-.233.72-.6.72-.387 0-.547-.267-.727-.58zm3.987-.12c.333.653 1.007 1.153 2.06 1.153 1.067 0 1.867-.553 1.867-1.573 0-.94-.54-1.36-1.5-1.773l-.28-.12c-.487-.207-.694-.347-.694-.68 0-.274.207-.487.54-.487.32 0 .534.14.727.487l.873-.58c-.366-.64-.886-.887-1.6-.887-1.006 0-1.653.64-1.653 1.487 0 .92.54 1.353 1.353 1.7l.28.12c.52.226.827.366.827.753 0 .32-.3.553-.767.553-.553 0-.873-.286-1.113-.686z"/></svg>', language: 'javascript' }
{
id: "html",
label: "HTML",
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#E65100" d="m4 4 2 22 10 2 10-2 2-22Zm19.72 7H11.28l.29 3h11.86l-.802 9.335L15.99 25l-6.635-1.646L8.93 19h3.02l.19 2 3.86.77 3.84-.77.29-4H8.84L8 8h16Z"/></svg>',
language: "markup",
},
{
id: "css",
label: "CSS",
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#42a5f5" d="m29.18 4-3.57 18.36-.33 1.64-4.74 1.57-3.28 1.09L13.21 28 2.87 24.05 4.05 18h4.2l-.44 2.85 6.34 2.42.78-.26 6.52-2.16.17-.83.79-4.02H4.44l.74-3.76.05-.24h17.96l.78-4H6l.78-4z"/></svg>',
language: "css",
},
{
id: "js",
label: "JavaScript",
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#ffca28" d="M2 2h12v12H2zm3.153 10.027c.267.567.794 1.033 1.694 1.033 1 0 1.686-.533 1.686-1.7V7.507H7.4v3.827c0 .573-.233.72-.6.72-.387 0-.547-.267-.727-.58zm3.987-.12c.333.653 1.007 1.153 2.06 1.153 1.067 0 1.867-.553 1.867-1.573 0-.94-.54-1.36-1.5-1.773l-.28-.12c-.487-.207-.694-.347-.694-.68 0-.274.207-.487.54-.487.32 0 .534.14.727.487l.873-.58c-.366-.64-.886-.887-1.6-.887-1.006 0-1.653.64-1.653 1.487 0 .92.54 1.353 1.353 1.7l.28.12c.52.226.827.366.827.753 0 .32-.3.553-.767.553-.553 0-.873-.286-1.113-.686z"/></svg>',
language: "javascript",
},
],
extraIcons: [
{ visible: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z"/></svg>`},
{ hidden: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z"/></svg>`}
{
visible: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z"/></svg>`,
},
{
hidden: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z"/></svg>`,
},
{
console: `
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H160v400Zm140-40-56-56 103-104-104-104 57-56 160 160-160 160Zm180 0v-80h240v80H480Z"/></svg>`
}
],
highlightedCode: {
html: '',
css: '',
js: ''
html: "",
css: "",
js: "",
},
showEditor: true,
showPreview: true
showPreview: true,
isExecutionPaused: false,
updateTimer: null,
showConsole: true,
playIcon: `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path fill="currentColor" d="M320-200v-560l440 280-440 280Zm80-280Zm0 134 210-134-210-134v268Z"/></svg>`,
pauseIcon: `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path fill="currentColor" d="M520-200v-560h240v560H520Zm-320 0v-560h240v560H200Zm400-80h80v-400h-80v400Zm-320 0h80v-400h-80v400Zm320-400Zm-320 0Z"/></svg>`,
};
},
@ -49,7 +88,7 @@ createApp({
return {
html: this.highlightedCode.html || this.html,
css: this.highlightedCode.css || this.css,
js: this.highlightedCode.js || this.js
js: this.highlightedCode.js || this.js,
};
},
currentCode: {
@ -59,39 +98,47 @@ createApp({
set(value) {
this[this.activeTab] = value;
this.highlightCode(value, this.activeTab);
}
},
},
previewWidth() {
if (typeof this.editorWidth === 'string' && this.editorWidth.endsWith('%')) {
return (100 - parseInt(this.editorWidth)) + '%';
if (
typeof this.editorWidth === "string" &&
this.editorWidth.endsWith("%")
) {
return 100 - parseInt(this.editorWidth) + "%";
}
return `calc(100% - ${this.editorWidth}px)`;
},
currentLanguage() {
const tab = this.tabs.find(t => t.id === this.activeTab);
return tab ? tab.language : 'markup';
}
const tab = this.tabs.find((t) => t.id === this.activeTab);
return tab ? tab.language : "markup";
},
},
watch: {
html(newVal) {
this.updatePreviewDebounced();
html: {
handler() {
this.schedulePreviewUpdate();
},
},
css(newVal) {
this.updatePreviewDebounced();
css: {
handler() {
this.schedulePreviewUpdate();
},
},
js: {
handler() {
this.schedulePreviewUpdate();
},
},
js(newVal) {
this.updatePreviewDebounced();
}
},
created() {
this.highlightCodeDebounced = this.debounce(this.highlightCode, 50);
this.updatePreviewDebounced = this.debounce(this.updatePreview, 50);
this.updatePreviewDebounced = this.debounce(this.updatePreview, 50);
const urlParams = new URLSearchParams(window.location.search);
const shareId = urlParams.get('share');
const shareId = urlParams.get("share");
if (shareId) {
this.loadSharedSnippet(shareId);
}
@ -99,76 +146,112 @@ createApp({
mounted() {
this.initializeLayout();
document.addEventListener('keydown', this.handleKeyboardShortcuts);
document.addEventListener("keydown", this.handleKeyboardShortcuts);
// Initialize syntax highlighting
this.highlightCode(this.html, 'html');
this.highlightCode(this.css, 'css');
this.highlightCode(this.js, 'js');
// initialize syntax highlighting
this.highlightCode(this.html, "html");
this.highlightCode(this.css, "css");
this.highlightCode(this.js, "js");
// ensure iframe isloaded before updating the preview
const preview = document.getElementById('preview-frame');
const preview = document.getElementById("preview-frame");
if (preview) {
preview.onload = () => {
this.updatePreview();
}
};
}
},
methods: {
handleScroll(event) {
const textarea = event.target;
const pre = textarea.nextElementSibling;
if (pre) {
pre.scrollTop = textarea.scrollTop;
pre.scrollLeft = textarea.scrollLeft
}
},
toggleConsole() {
this.showConsole = !this.showConsole;
this.updatePreview();
},
schedulePreviewUpdate() {
if (this.updateTimer) {
clearTimeout(this.updateTimer);
}
if (!this.isExecutionPaused) {
this.updateTimer = setTimeout(() => {
this.updatePreview();
}, 500);
}
},
toggleExecution() {
this.isExecutionPaused = !this.isExecutionPaused;
if (!this.isExecutionPaused) {
// resume execution
this.updatePreview();
}
},
toggleEditor() {
this.showEditor = !this.showEditor;
},
togglePreview() {
this.showPreview = !this.showPreview;
if (!this.showPreview) { this.editorWidth = '100%';}
else { this.editorWidth = '50%'; }
if (!this.showPreview) {
this.editorWidth = "100%";
} else {
this.editorWidth = "50%";
}
},
highlightCode(code, tab) {
const languageMap = {
html: 'markup',
css: 'css',
js: 'javascript'
html: "markup",
css: "css",
js: "javascript",
};
const language = languageMap[tab];
if (!language) return;
// run highlighting in a requestAnimationFrame to avoid blocking the main thread
requestAnimationFrame(() => {
try {
this.highlightedCode[tab] = Prism.highlight(
code || '',
code || "",
Prism.languages[language],
language
);
} catch (error) {
console.error('Highlighting error:', error);
this.highlightedCode[tab] = code || '';
console.error("Highlighting error:", error);
this.highlightedCode[tab] = code || "";
}
});
},
handleKeydown(event) {
if (event.key === 'Tab') {
if (event.key === "Tab") {
event.preventDefault();
const textarea = event.target;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const spaces = ' ';
const spaces = " ";
const value = this.currentCode;
const beforeCursor = value.substring(0, start);
const afterCursor = value.substring(end);
if (event.shiftKey) {
// TODO: Shift + Tab undo 2 space
} else {
// Handle Tab (indent)
this.currentCode = beforeCursor + spaces + afterCursor;
this.$nextTick(() => {
textarea.selectionStart = textarea.selectionEnd = start + spaces.length;
textarea.selectionStart = textarea.selectionEnd =
start + spaces.length;
});
}
}
@ -178,7 +261,7 @@ createApp({
this[this.activeTab] = value;
this.highlightCode(value, this.activeTab);
},
debounce(fn, delay) {
let timeoutId;
return function (...args) {
@ -188,7 +271,7 @@ createApp({
},
handleKeyboardShortcuts(e) {
// Handle Ctrl/Cmd + number for tab switching
// handle Ctrl/Cmd + number for tab switching
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
const num = parseInt(e.key);
if (num >= 1 && num <= this.tabs.length) {
@ -197,25 +280,25 @@ createApp({
}
}
// Handle Ctrl/Cmd + S for save
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
// handle Ctrl/Cmd + S for save
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
e.preventDefault();
this.saveSnippet();
}
},
initializeLayout() {
const container = document.querySelector('.editor-container');
const container = document.querySelector(".editor-container");
this.containerWidth = container.offsetWidth;
this.maxWidth = this.containerWidth - this.minWidth;
this.updateLayout();
},
updateLayout() {
const editorGroup = document.querySelector('.editor-group');
const preview = document.querySelector('.preview');
const editorGroup = document.querySelector(".editor-group");
const preview = document.querySelector(".preview");
if (editorGroup && preview) {
editorGroup.style.width = this.editorWidth;
preview.style.width = this.previewWidth;
@ -223,24 +306,56 @@ createApp({
},
updatePreview() {
const preview = document.getElementById('preview-frame');
if (this.isExecutionPaused) return;
const preview = document.getElementById("preview-frame");
if (!preview) return;
const doc = preview.contentDocument;
// create a new iframe to replace the existing one
const newFrame = document.createElement("iframe");
newFrame.id = "preview-frame";
// replace the old frame with the new one
preview.parentNode.replaceChild(newFrame, preview);
// write content to the new frame
const doc = newFrame.contentDocument;
doc.open();
doc.write(`
const content = `
<!DOCTYPE html>
<html>
<head>
<style>${this.css}</style>
<style>${this.css || ""}</style>
<!-- Eruda console for debugging -->
<script src="/libraries/eruda/eruda.min.js"></script>
</head>
<body>
${this.html}
<script>${this.js}<\/script>
${this.html || ""}
${this.showConsole ? `<script>window.eruda && eruda.init({
useShadowDom: true,
autoScale: true,
defaults: {
displaySize: 50,
transparency: 0.9,
theme: 'Monokai Pro'
}
});
eruda.show();
</script>` : ''}
<script>
${this.js || ""}
</script>
</body>
</html>
`);
doc.close();
</html>`;
try {
doc.write(content);
doc.close();
} catch (error) {
console.error("Preview update error:", error);
}
},
switchTab(tabId) {
@ -248,36 +363,36 @@ createApp({
// re-highlight code when switching tabs
this.$nextTick(() => {
this.highlightCode(this[tabId], tabId);
const editor = document.querySelector('.editor-content textarea');
const editor = document.querySelector(".editor-content textarea");
if (editor) editor.focus();
});
},
async login() {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: this.loginEmail,
password: this.loginPassword
})
password: this.loginPassword,
}),
});
if (!response.ok) throw new Error('Login failed');
if (!response.ok) throw new Error("Login failed");
const data = await response.json();
this.token = data.token;
localStorage.setItem('token', data.token);
localStorage.setItem("token", data.token);
this.showLogin = false;
} catch (error) {
alert('Login failed');
alert("Login failed");
}
},
logout() {
this.token = null;
localStorage.removeItem('token');
localStorage.removeItem("token");
},
async saveSnippet() {
@ -287,35 +402,35 @@ createApp({
}
try {
const response = await fetch('/api/snippets', {
method: 'POST',
const response = await fetch("/api/snippets", {
method: "POST",
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
"Content-Type": "application/json",
Authorization: `Bearer ${this.token}`,
},
body: JSON.stringify({
title: this.title || 'Untitled',
title: this.title || "Untitled",
html: this.html,
css: this.css,
js: this.js
})
js: this.js,
}),
});
if (!response.ok) throw new Error('Failed to save snippet');
if (!response.ok) throw new Error("Failed to save snippet");
const data = await response.json();
this.currentShareId = data.shareId;
this.shareUrl = `${window.location.origin}/?share=${data.shareId}`;
this.showShareModal = true;
} catch (error) {
alert('Failed to save snippet');
alert("Failed to save snippet");
}
},
async loadSharedSnippet(shareId) {
try {
const response = await fetch(`/api/snippets/share/${shareId}`);
if (!response.ok) throw new Error('Failed to load snippet');
if (!response.ok) throw new Error("Failed to load snippet");
const data = await response.json();
this.title = data.title;
@ -323,13 +438,13 @@ createApp({
this.css = data.css;
this.js = data.js;
this.highlightCode(this.html, 'html');
this.highlightCode(this.css, 'css');
this.highlightCode(this.js, 'js');
this.highlightCode(this.html, "html");
this.highlightCode(this.css, "css");
this.highlightCode(this.js, "js");
this.updatePreview();
} catch (error) {
alert('Failed to load shared snippet');
alert("Failed to load shared snippet");
}
},
@ -337,8 +452,8 @@ createApp({
try {
await navigator.clipboard.writeText(this.shareUrl);
} catch (error) {
alert('Failed to copy URL');
alert("Failed to copy URL");
}
}
}
}).mount('#app');
},
},
}).mount("#app");

1
public/bincode-min.svg Normal file

@ -0,0 +1 @@
<svg width="100" height="100" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M87.5 0h-75C5.596 0 0 5.596 0 12.5v75C0 94.404 5.596 100 12.5 100h75c6.904 0 12.5-5.596 12.5-12.5v-75C100 5.596 94.404 0 87.5 0z" fill="#DEC915"/><path d="M18.75 23.75a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM37.5 23.75a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM56.25 23.75a5 5 0 1 0 0-10 5 5 0 0 0 0 10z" fill="#fff"/><path d="m25 50 11.086 11.086a2 2 0 0 1 0 2.828L25 75" stroke="#fff" stroke-width="6.25"/><path d="M50 75h21a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2H50a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2z" fill="#fff" stroke="#fff" stroke-width="1.25"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h100v100H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 719 B

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
public/font.css Normal file

File diff suppressed because one or more lines are too long

@ -4,7 +4,13 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BinCode</title>
<link rel="icon" type="image/png" href="./logo/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="./logo/favicon.svg" />
<link rel="shortcut icon" href="./logo/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="stylesheet" href="/style.css" />
<link rel="stylesheet" href="./font.css">
<!-- Prismjs css -->
<link rel="stylesheet" href="./libraries/prism/theme/prism.min.css" />
</head>
@ -25,6 +31,20 @@
<span v-html="showPreview ? extraIcons[0].visible : extraIcons[1].hidden"></span>
<span>preview</span>
</button>
<button class="toggle-icon" @click="toggleConsole">
<span v-html="showConsole ? extraIcons[2].console : extraIcons[2].console"></span>
<span>console</span>
</button>
<button
class="toggle-icon"
@click="toggleExecution"
:class="{ 'toggle-active': isExecutionPaused }"
>
<span v-html="isExecutionPaused ? playIcon : pauseIcon"></span>
<span>{{ isExecutionPaused ? 'Resume' : 'Pause' }}</span>
</button>
</div>
<div class="nav-actions">
<div class="auth-buttons" v-if="!token">
@ -62,6 +82,7 @@
<textarea
:value="currentCode"
@input="handleInput"
@scroll="handleScroll"
@keydown="handleKeydown"
:class="{ 'active-editor': true }"
spellcheck="false"
@ -82,7 +103,8 @@
>
<iframe id="preview-frame"></iframe>
</div>
</div>
</div>
<!-- Share Modal -->
<div

1
public/libraries/eruda/eruda.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/logo/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

17
public/logo/favicon.svg Normal file

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="100" height="100"><svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_126_85)">
<path d="M87.5 0H12.5C5.59644 0 0 5.59644 0 12.5V87.5C0 94.4036 5.59644 100 12.5 100H87.5C94.4036 100 100 94.4036 100 87.5V12.5C100 5.59644 94.4036 0 87.5 0Z" fill="#DEC915"></path>
<path d="M18.75 23.75C21.5114 23.75 23.75 21.5114 23.75 18.75C23.75 15.9886 21.5114 13.75 18.75 13.75C15.9886 13.75 13.75 15.9886 13.75 18.75C13.75 21.5114 15.9886 23.75 18.75 23.75Z" fill="white"></path>
<path d="M37.5 23.75C40.2614 23.75 42.5 21.5114 42.5 18.75C42.5 15.9886 40.2614 13.75 37.5 13.75C34.7386 13.75 32.5 15.9886 32.5 18.75C32.5 21.5114 34.7386 23.75 37.5 23.75Z" fill="white"></path>
<path d="M56.25 23.75C59.0114 23.75 61.25 21.5114 61.25 18.75C61.25 15.9886 59.0114 13.75 56.25 13.75C53.4886 13.75 51.25 15.9886 51.25 18.75C51.25 21.5114 53.4886 23.75 56.25 23.75Z" fill="white"></path>
<path d="M25 50L36.0858 61.0858C36.8668 61.8668 36.8668 63.1332 36.0858 63.9142L25 75" stroke="white" stroke-width="6.25"></path>
<path d="M50 75H71C72.1046 75 73 74.1046 73 73V71C73 69.8954 72.1046 69 71 69H50C48.8954 69 48 69.8954 48 71V73C48 74.1046 48.8954 75 50 75Z" fill="white" stroke="white" stroke-width="1.25"></path>
</g>
<defs>
<clipPath id="SvgjsClipPath1009">
<rect width="100" height="100" fill="white"></rect>
</clipPath>
</defs>
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
@media (prefers-color-scheme: dark) { :root { filter: none; } }
</style></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -1,3 +1,63 @@
/* Fonts */
/*! system-font.css v1.1.0 | CC0-1.0 License | github.com/jonathantneal/system-font-face */
@font-face {
font-family: system;
font-style: normal;
font-weight: 300;
src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), local("DroidSans"), local("Tahoma");
}
@font-face {
font-family: system;
font-style: italic;
font-weight: 300;
src: local(".SFNSText-LightItalic"), local(".HelveticaNeueDeskInterface-Italic"), local(".LucidaGrandeUI"), local("Ubuntu Light Italic"), local("Segoe UI Light Italic"), local("Roboto-LightItalic"), local("DroidSans"), local("Tahoma");
}
@font-face {
font-family: system;
font-style: normal;
font-weight: 400;
src: local(".SFNSText-Regular"), local(".HelveticaNeueDeskInterface-Regular"), local(".LucidaGrandeUI"), local("Ubuntu"), local("Segoe UI"), local("Roboto-Regular"), local("DroidSans"), local("Tahoma");
}
@font-face {
font-family: system;
font-style: italic;
font-weight: 400;
src: local(".SFNSText-Italic"), local(".HelveticaNeueDeskInterface-Italic"), local(".LucidaGrandeUI"), local("Ubuntu Italic"), local("Segoe UI Italic"), local("Roboto-Italic"), local("DroidSans"), local("Tahoma");
}
@font-face {
font-family: system;
font-style: normal;
font-weight: 500;
src: local(".SFNSText-Medium"), local(".HelveticaNeueDeskInterface-MediumP4"), local(".LucidaGrandeUI"), local("Ubuntu Medium"), local("Segoe UI Semibold"), local("Roboto-Medium"), local("DroidSans-Bold"), local("Tahoma Bold");
}
@font-face {
font-family: system;
font-style: italic;
font-weight: 500;
src: local(".SFNSText-MediumItalic"), local(".HelveticaNeueDeskInterface-MediumItalicP4"), local(".LucidaGrandeUI"), local("Ubuntu Medium Italic"), local("Segoe UI Semibold Italic"), local("Roboto-MediumItalic"), local("DroidSans-Bold"), local("Tahoma Bold");
}
@font-face {
font-family: system;
font-style: normal;
font-weight: 700;
src: local(".SFNSText-Bold"), local(".HelveticaNeueDeskInterface-Bold"), local(".LucidaGrandeUI"), local("Ubuntu Bold"), local("Roboto-Bold"), local("DroidSans-Bold"), local("Segoe UI Bold"), local("Tahoma Bold");
}
@font-face {
font-family: system;
font-style: italic;
font-weight: 700;
src: local(".SFNSText-BoldItalic"), local(".HelveticaNeueDeskInterface-BoldItalic"), local(".LucidaGrandeUI"), local("Ubuntu Bold Italic"), local("Roboto-BoldItalic"), local("DroidSans-Bold"), local("Segoe UI Bold Italic"), local("Tahoma Bold");
}
:root {
--primary: #63DEAB;
--primary-dark: #48de9f;
@ -46,7 +106,8 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-family: system, "Helvetica Neue", Helvetica, Arial;
font-size: 13px;
color: var(--text);
line-height: 1.5;
height: 100vh;
@ -116,7 +177,7 @@ nav {
flex-direction: column;
min-width: 250px;
background: var(--background);
transition: width 0.2s ease;
transition: width 0.15s ease;
}
/* Tab bar */
@ -178,7 +239,7 @@ nav {
.editor-content textarea,
.editor-content pre {
box-sizing: border-box;
padding: 1rem;
padding: 0.15rem;
margin: 0;
border: 0;
width: 100%;
@ -188,7 +249,6 @@ nav {
left: 0;
overflow: auto;
white-space: pre;
font-family: Monaco, 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
}
@ -239,7 +299,7 @@ textarea::placeholder {
width: 2px;
background: var(--border);
transform: translateX(-50%);
transition: background-color 0.2s;
transition: background-color 0.15s;
}
body.resizing {
@ -265,7 +325,7 @@ body.resizing .resize-handle::after {
min-width: 250px;
background: var(--background);
border-left: 1px solid var(--border);
transition: width 0.2s ease;
transition: width 0.15s ease;
}
iframe {
@ -275,6 +335,10 @@ iframe {
background: white;
}
.eruda-icon-tool {
display: none!important;
}
/* Keyboard shortcuts */
kbd {
padding: 0.1rem 0.4rem;
@ -300,7 +364,7 @@ button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
transition: all 0.15s;
}
button:hover {
@ -418,7 +482,7 @@ form > button {
color: white;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
transition: all 0.15s;
}
.share-url input {
@ -444,7 +508,7 @@ form > button {
align-items: center;
gap: 0.5rem;
color: var(--text-light);
transition: all 0.2s;
transition: all 0.15s;
}
.view-toggles button:hover {
@ -477,7 +541,7 @@ form > button {
display: flex;
flex-direction: column;
background: var(--background);
transition: all 0.2s ease;
transition: all 0.15s ease;
width: 100%;
}
@ -491,7 +555,7 @@ form > button {
flex: 1;
background: var(--background);
border-left: 1px solid var(--border);
transition: all 0.3s ease;
transition: all 0.15s ease;
}
.preview.hidden {
@ -550,6 +614,13 @@ form > button {
color: #000;
}
#console-root {
position: absolute;
bottom: 0;
right: 0;
width: 100%;
z-index: 100;
}
/* Responsive */
@ -612,7 +683,11 @@ form > button {
background: var(--primary-light);
color: var(--text);
}
console-dark {
--console-bg: #1e1e1e;
--console-header: #2d2d2d;
--console-text: #ffffff;
--console-border: #404040;
}
}

@ -21,10 +21,7 @@ router.post('/login', validateAuth, async (req, res, next) => {
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
console.log('User found:', { id: user.id, email: user.email });
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });

@ -28,8 +28,6 @@ router.post('/', auth, validateSnippet, async (req, res) => {
router.get('/share/:shareId', async (req, res) => {
const { shareId } = req.params;
const db = getDb();
console.log(shareId);
db.get(
snippetQueries.getByShareId,

@ -10,16 +10,14 @@ app.set('trust proxy', true);
app.use(rateLimit({
windowMs: 5 * 60 * 1000, // 5min
max: 1000000, // 100 request per IP
max: 100, // 100 request per IP
}));
app.use(express.json());
app.use(express.static('public'));
// routes
app.use('/api', routes);
// error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something broke!' });