diff --git a/middleware/validation.js b/middleware/validation.js
index 9d455aa..2d37b3d 100644
--- a/middleware/validation.js
+++ b/middleware/validation.js
@@ -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
diff --git a/public/app.js b/public/app.js
index 191a10b..e506160 100644
--- a/public/app.js
+++ b/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: `
+
+
+
+
+ Bin Code
+
+
+
+
+
+ `,
+ 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: '', language: 'markup' },
- { id: 'css', label: 'CSS', icon: '', language: 'css' },
- { id: 'js', label: 'JavaScript', icon: '', language: 'javascript' }
+ {
+ id: "html",
+ label: "HTML",
+ icon: '',
+ language: "markup",
+ },
+ {
+ id: "css",
+ label: "CSS",
+ icon: '',
+ language: "css",
+ },
+ {
+ id: "js",
+ label: "JavaScript",
+ icon: '',
+ language: "javascript",
+ },
],
extraIcons: [
- { visible: ``},
- { hidden: ``}
+ {
+ visible: ``,
+ },
+ {
+ hidden: ``,
+ },
+ {
+ console: `
+ `
+ }
],
highlightedCode: {
- html: '',
- css: '',
- js: ''
+ html: "",
+ css: "",
+ js: "",
},
showEditor: true,
- showPreview: true
+ showPreview: true,
+ isExecutionPaused: false,
+ updateTimer: null,
+ showConsole: true,
+ playIcon: ``,
+ pauseIcon: ``,
};
},
@@ -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 = `
-
+
+
+
- ${this.html}
- ` : ''}
+
-
- `);
- doc.close();
+