BinCode/public/app.js
2025-01-17 15:05:21 -05:00

329 lines
8.4 KiB
JavaScript

const { createApp } = Vue;
Prism.manual = true;
createApp({
data() {
return {
token: localStorage.getItem('token'),
showLogin: false,
showShareModal: false,
loginEmail: '',
loginPassword: '',
title: '',
html: '',
css: '',
js: '',
shareUrl: '',
currentShareId: null,
isDragging: false,
startX: null,
startWidth: null,
containerWidth: null,
editorWidth: '50%',
minWidth: 250,
maxWidth: null,
// Tab state
activeTab: 'html',
tabs: [
{ id: 'html', label: 'HTML', icon: '📄', language: 'markup' },
{ id: 'css', label: 'CSS', icon: '🎨', language: 'css' },
{ id: 'js', label: 'JavaScript', icon: '⚡', language: 'javascript' }
],
highlightedCode: {
html: '',
css: '',
js: ''
},
showEditor: true,
showPreview: true
};
},
computed: {
currentCode: {
get() {
return this[this.activeTab];
},
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)) + '%';
}
return `calc(100% - ${this.editorWidth}px)`;
},
currentLanguage() {
const tab = this.tabs.find(t => t.id === this.activeTab);
return tab ? tab.language : 'markup';
}
},
watch: {
html(newVal) {
this.updatePreviewDebounced();
},
css(newVal) {
this.updatePreviewDebounced();
},
js(newVal) {
this.updatePreviewDebounced();
}
},
created() {
this.updatePreviewDebounced = this.debounce(this.updatePreview, 300);
const urlParams = new URLSearchParams(window.location.search);
const shareId = urlParams.get('share');
if (shareId) {
this.loadSharedSnippet(shareId);
}
},
mounted() {
this.initializeLayout();
document.addEventListener('keydown', this.handleKeyboardShortcuts);
// 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');
if (preview) {
preview.onload = () => {
this.updatePreview();
}
}
},
methods: {
toggleEditor() {
this.showEditor = !this.showEditor;
},
togglePreview() {
this.showPreview = !this.showPreview;
if (!this.showPreview) { this.editorWidth = '100%';}
else { this.editorWidth = '50%'; }
},
highlightCode(code, tab) {
const languageMap = {
html: 'markup',
css: 'css',
js: 'javascript'
};
const language = languageMap[tab];
if (!language) return;
try {
this.highlightedCode[tab] = Prism.highlight(
code || '',
Prism.languages[language],
language
);
} catch (error) {
console.error('Highlighting error:', error);
this.highlightedCode[tab] = code || '';
}
},
handleKeydown(event) {
if (event.key === 'Tab') {
event.preventDefault();
const textarea = event.target;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
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;
});
}
}
},
handleInput(event) {
const value = event.target.value;
this[this.activeTab] = value;
this.highlightCode(value, this.activeTab);
},
debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
},
handleKeyboardShortcuts(e) {
// 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) {
this.activeTab = this.tabs[num - 1].id;
e.preventDefault();
}
}
// 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');
this.containerWidth = container.offsetWidth;
this.maxWidth = this.containerWidth - this.minWidth;
this.updateLayout();
},
updateLayout() {
const editorGroup = document.querySelector('.editor-group');
const preview = document.querySelector('.preview');
if (editorGroup && preview) {
editorGroup.style.width = this.editorWidth;
preview.style.width = this.previewWidth;
}
},
updatePreview() {
const preview = document.getElementById('preview-frame');
if (!preview) return;
const doc = preview.contentDocument;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>${this.css}</style>
</head>
<body>
${this.html}
<script>${this.js}<\/script>
</body>
</html>
`);
doc.close();
},
switchTab(tabId) {
this.activeTab = tabId;
// re-highlight code when switching tabs
this.$nextTick(() => {
this.highlightCode(this[tabId], tabId);
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' },
body: JSON.stringify({
email: this.loginEmail,
password: this.loginPassword
})
});
if (!response.ok) throw new Error('Login failed');
const data = await response.json();
this.token = data.token;
localStorage.setItem('token', data.token);
this.showLogin = false;
} catch (error) {
alert('Login failed');
}
},
logout() {
this.token = null;
localStorage.removeItem('token');
},
async saveSnippet() {
if (!this.token) {
this.showLogin = true;
return;
}
try {
const response = await fetch('/api/snippets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({
title: this.title || 'Untitled',
html: this.html,
css: this.css,
js: this.js
})
});
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');
}
},
async loadSharedSnippet(shareId) {
try {
const response = await fetch(`/api/snippets/share/${shareId}`);
if (!response.ok) throw new Error('Failed to load snippet');
const data = await response.json();
this.title = data.title;
this.html = data.html;
this.css = data.css;
this.js = data.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');
}
},
async copyShareUrl() {
try {
await navigator.clipboard.writeText(this.shareUrl);
} catch (error) {
alert('Failed to copy URL');
}
}
}
}).mount('#app');