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:
parent
8a2b45099a
commit
52f6af96f7
@ -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
|
||||
|
331
public/app.js
331
public/app.js
@ -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
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
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
1
public/font.css
Normal file
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
1
public/libraries/eruda/eruda.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
public/logo/apple-touch-icon.png
Normal file
BIN
public/logo/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
public/logo/favicon-96x96.png
Normal file
BIN
public/logo/favicon-96x96.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
public/logo/favicon.ico
Normal file
BIN
public/logo/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
17
public/logo/favicon.svg
Normal file
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 |
103
public/style.css
103
public/style.css
@ -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!' });
|
||||
|
Loading…
Reference in New Issue
Block a user