diff --git a/package-lock.json b/package-lock.json index 0ee029d..524dc23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,9 @@ "express-session": "^1.18.2", "helmet": "^8.1.0", "node-pty": "^1.1.0", - "socket.io": "^4.8.3" + "socket.io": "^4.8.3", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0" }, "engines": { "node": ">=20.0.0", @@ -1745,6 +1747,23 @@ "optional": true } } + }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "license": "MIT" + }, + "node_modules/xterm-addon-fit": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz", + "integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", + "license": "MIT", + "peerDependencies": { + "xterm": "^5.0.0" + } } } } diff --git a/package.json b/package.json index 4bed9c7..3cbf462 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "express-session": "^1.18.2", "helmet": "^8.1.0", "node-pty": "^1.1.0", - "socket.io": "^4.8.3" + "socket.io": "^4.8.3", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0" } } diff --git a/public/app.js b/public/app.js index f2ba763..e7a52ce 100644 --- a/public/app.js +++ b/public/app.js @@ -169,9 +169,15 @@ function openWindow(appName) { focusWindow(appName); updateTaskbar(); - // Initialize terminal if opening terminal window + // Initialize app-specific functionality if (appName === 'terminal' && !terminal) { setTimeout(initializeTerminal, 100); + } else if (appName === 'files') { + setTimeout(loadFilesList, 100); + } else if (appName === 'notes') { + setTimeout(loadNotes, 100); + } else if (appName === 'calculator') { + setTimeout(updateCalcDisplay, 100); } // Hide start menu @@ -234,6 +240,10 @@ function updateTaskbar() { const appNames = { terminal: { icon: '๐Ÿ’ป', name: 'Terminal' }, browser: { icon: '๐ŸŒ', name: 'Browser' }, + editor: { icon: '๐Ÿ“', name: 'Text Editor' }, + files: { icon: '๐Ÿ“', name: 'Files' }, + calculator: { icon: '๐Ÿ”ข', name: 'Calculator' }, + notes: { icon: '๐Ÿ“”', name: 'Notes' }, store: { icon: '๐Ÿ“ฆ', name: 'App Store' }, apps: { icon: '๐Ÿ“ฑ', name: 'My Apps' }, activity: { icon: '๐Ÿ“Š', name: 'Activity' }, @@ -649,6 +659,298 @@ async function checkAuth() { } } +// ==================== TEXT EDITOR FUNCTIONALITY ==================== +let currentEditorFile = null; + +async function saveEditorFile() { + const filename = document.getElementById('editor-filename').value || 'untitled.txt'; + const content = document.getElementById('editor-content').value; + + try { + await fetch('/api/files', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: filename, content, type: 'text' }) + }); + showToast(`File "${filename}" saved successfully!`); + currentEditorFile = filename; + loadFilesList(); + } catch (error) { + showToast('Failed to save file', 'error'); + } +} + +function newEditorFile() { + document.getElementById('editor-filename').value = ''; + document.getElementById('editor-content').value = ''; + currentEditorFile = null; + showToast('New file created'); +} + +function clearEditor() { + if (confirm('Clear all content?')) { + document.getElementById('editor-content').value = ''; + } +} + +// ==================== FILE MANAGER FUNCTIONALITY ==================== +async function loadFilesList() { + try { + const response = await fetch('/api/files'); + const files = await response.json(); + + const filesList = document.getElementById('files-list'); + if (!filesList) return; + + if (files.length === 0) { + filesList.innerHTML = '

No files yet. Create files in the Text Editor.

'; + return; + } + + filesList.innerHTML = files.map(file => ` +
+
+

${escapeHtml(file.file_path)}

+

Modified: ${new Date(file.updated_at).toLocaleString()}

+
+
+ + +
+
+ `).join(''); + + // Add event listeners + filesList.querySelectorAll('.file-open-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + openFile(btn.dataset.path); + }); + }); + + filesList.querySelectorAll('.file-delete-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + deleteFile(btn.dataset.path); + }); + }); + } catch (error) { + const filesList = document.getElementById('files-list'); + if (filesList) { + filesList.innerHTML = '

Failed to load files

'; + } + } +} + +async function openFile(filepath) { + try { + const response = await fetch(`/api/files?path=${encodeURIComponent(filepath)}`); + const file = await response.json(); + + if (file) { + // Open editor and load content + openWindow('editor'); + document.getElementById('editor-filename').value = file.file_path; + document.getElementById('editor-content').value = file.file_content || ''; + currentEditorFile = file.file_path; + showToast(`Opened "${file.file_path}"`); + } + } catch (error) { + showToast('Failed to open file', 'error'); + } +} + +async function deleteFile(filepath) { + if (!confirm(`Delete "${filepath}"?`)) return; + + try { + await fetch('/api/files', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: filepath }) + }); + showToast(`File "${filepath}" deleted`); + loadFilesList(); + } catch (error) { + showToast('Failed to delete file', 'error'); + } +} + +// ==================== CALCULATOR FUNCTIONALITY ==================== +let calcDisplay = '0'; +let calcPrevValue = null; +let calcOperator = null; +let calcWaitingForOperand = false; + +function updateCalcDisplay() { + document.getElementById('calc-display').textContent = calcDisplay; +} + +function handleCalcButton(action) { + if (!isNaN(action) || action === '.') { + // Number or decimal point + if (calcWaitingForOperand) { + calcDisplay = action; + calcWaitingForOperand = false; + } else { + calcDisplay = calcDisplay === '0' ? action : calcDisplay + action; + } + } else if (action === 'clear') { + calcDisplay = '0'; + calcPrevValue = null; + calcOperator = null; + calcWaitingForOperand = false; + } else if (action === 'backspace') { + calcDisplay = calcDisplay.slice(0, -1) || '0'; + } else if (action === '=') { + if (calcOperator && calcPrevValue !== null) { + const curr = parseFloat(calcDisplay); + const prev = parseFloat(calcPrevValue); + + switch (calcOperator) { + case '+': calcDisplay = String(prev + curr); break; + case '-': calcDisplay = String(prev - curr); break; + case '*': calcDisplay = String(prev * curr); break; + case '/': calcDisplay = curr !== 0 ? String(prev / curr) : 'Error'; break; + } + + calcPrevValue = null; + calcOperator = null; + calcWaitingForOperand = true; + } + } else { + // Operator + if (calcOperator && !calcWaitingForOperand) { + // Calculate first + const curr = parseFloat(calcDisplay); + const prev = parseFloat(calcPrevValue); + + switch (calcOperator) { + case '+': calcDisplay = String(prev + curr); break; + case '-': calcDisplay = String(prev - curr); break; + case '*': calcDisplay = String(prev * curr); break; + case '/': calcDisplay = curr !== 0 ? String(prev / curr) : 'Error'; break; + } + } + + calcPrevValue = calcDisplay; + calcOperator = action; + calcWaitingForOperand = true; + } + + updateCalcDisplay(); +} + +// ==================== NOTES FUNCTIONALITY ==================== +let notes = []; +let currentNoteId = null; + +async function loadNotes() { + try { + const response = await fetch('/api/userdata/notes'); + const data = await response.json(); + notes = Object.entries(data) + .map(([id, note]) => ({ id, ...note })) + .filter(note => note.title || note.content); // Filter out deleted notes + renderNotesList(); + } catch (error) { + notes = []; + renderNotesList(); + } +} + +function renderNotesList() { + const notesList = document.getElementById('notes-list'); + if (!notesList) return; + + if (notes.length === 0) { + notesList.innerHTML = '

No notes yet. Click "New Note" to create one.

'; + return; + } + + notesList.innerHTML = notes.map(note => ` +
+

${escapeHtml(note.title || 'Untitled')}

+

${new Date(note.updated || Date.now()).toLocaleDateString()}

+
+ `).join(''); + + // Add event listeners + notesList.querySelectorAll('.notes-item').forEach(item => { + item.addEventListener('click', () => loadNote(item.dataset.id)); + }); +} + +function loadNote(noteId) { + const note = notes.find(n => n.id === noteId); + if (note) { + currentNoteId = noteId; + document.getElementById('notes-title').value = note.title || ''; + document.getElementById('notes-content').value = note.content || ''; + renderNotesList(); + } +} + +async function saveNote() { + const title = document.getElementById('notes-title').value; + const content = document.getElementById('notes-content').value; + + if (!title && !content) { + showToast('Please add a title or content', 'error'); + return; + } + + const noteId = currentNoteId || `note_${Date.now()}`; + const note = { title, content, updated: new Date().toISOString() }; + + try { + await fetch('/api/userdata', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dataType: 'notes', dataKey: noteId, dataValue: note }) + }); + + currentNoteId = noteId; + showToast('Note saved!'); + await loadNotes(); + } catch (error) { + showToast('Failed to save note', 'error'); + } +} + +async function createNewNote() { + currentNoteId = null; + document.getElementById('notes-title').value = ''; + document.getElementById('notes-content').value = ''; + showToast('New note created'); +} + +async function deleteNote() { + if (!currentNoteId) { + showToast('No note selected', 'error'); + return; + } + + if (!confirm('Delete this note?')) return; + + try { + // Mark as deleted by saving null value + await fetch('/api/userdata', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dataType: 'notes', dataKey: currentNoteId, dataValue: null }) + }); + + showToast('Note deleted'); + currentNoteId = null; + document.getElementById('notes-title').value = ''; + document.getElementById('notes-content').value = ''; + await loadNotes(); + } catch (error) { + showToast('Failed to delete note', 'error'); + } +} + // Initialize app document.addEventListener('DOMContentLoaded', () => { checkAuth(); @@ -656,7 +958,7 @@ document.addEventListener('DOMContentLoaded', () => { // Auth form event listeners document.getElementById('login-btn').addEventListener('click', login); document.getElementById('register-btn').addEventListener('click', register); - document.getElementById('show-register-link').addEventListener('click', (e) => { + document.getElementById('show-register-btn').addEventListener('click', (e) => { e.preventDefault(); showRegister(); }); @@ -766,4 +1068,33 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('register-password').addEventListener('keypress', (e) => { if (e.key === 'Enter') register(); }); + + // Text Editor event listeners + const editorSaveBtn = document.getElementById('editor-save-btn'); + const editorNewBtn = document.getElementById('editor-new-btn'); + const editorClearBtn = document.getElementById('editor-clear-btn'); + + if (editorSaveBtn) editorSaveBtn.addEventListener('click', saveEditorFile); + if (editorNewBtn) editorNewBtn.addEventListener('click', newEditorFile); + if (editorClearBtn) editorClearBtn.addEventListener('click', clearEditor); + + // File Manager event listeners + const filesRefreshBtn = document.getElementById('files-refresh-btn'); + if (filesRefreshBtn) filesRefreshBtn.addEventListener('click', loadFilesList); + + // Calculator event listeners + document.querySelectorAll('.calc-btn').forEach(btn => { + btn.addEventListener('click', () => { + handleCalcButton(btn.dataset.action); + }); + }); + + // Notes event listeners + const notesNewBtn = document.getElementById('notes-new-btn'); + const notesSaveBtn = document.getElementById('notes-save-btn'); + const notesDeleteBtn = document.getElementById('notes-delete-btn'); + + if (notesNewBtn) notesNewBtn.addEventListener('click', createNewNote); + if (notesSaveBtn) notesSaveBtn.addEventListener('click', saveNote); + if (notesDeleteBtn) notesDeleteBtn.addEventListener('click', deleteNote); }); diff --git a/public/index.html b/public/index.html index e7f5463..ab88bb7 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ CloudettteVM - Virtual Linux Machine - + @@ -15,58 +15,111 @@
-
-
โ˜๏ธ
-

CloudettteVM

-

Your Virtual Linux Desktop in the Cloud

-
- -
-
-
-

Welcome Back

-

Enter your credentials to continue

-
-
-
- - -
-
- - -
- +
+ +
+
+
โ˜๏ธ
+

CloudettteVM

+

Your Virtual Linux Desktop in the Cloud

- - +
+
๐Ÿ“
+ Text Editor +
+
+
๐Ÿ“
+ Files +
+
+
๐Ÿ”ข
+ Calculator +
+
+
๐Ÿ“”
+ Notes +
๐Ÿ“ฆ
App Store @@ -316,6 +385,122 @@

โ„น๏ธ About

+ + + + + + + + + + + +
@@ -349,6 +534,23 @@

โ„น๏ธ About

๐ŸŒ Browser
+
+ ๐Ÿ“ + Text Editor +
+
+ ๐Ÿ“ + Files +
+
+ ๐Ÿ”ข + Calculator +
+
+ ๐Ÿ“” + Notes +
+
๐Ÿ“ฆ App Store @@ -377,9 +579,9 @@

โ„น๏ธ About

- - - + + + diff --git a/public/styles.css b/public/styles.css index 1c32629..814a5f6 100644 --- a/public/styles.css +++ b/public/styles.css @@ -165,6 +165,32 @@ body { gap: 32px; } +.auth-content { + display: grid; + grid-template-columns: 420px 400px; + gap: 48px; + max-width: 920px; + width: 100%; + align-items: start; +} + +@media (max-width: 1024px) { + .auth-content { + grid-template-columns: 1fr; + max-width: 420px; + } + + .auth-right { + display: none; + } +} + +.auth-left { + display: flex; + flex-direction: column; + gap: 24px; +} + .auth-brand { text-align: center; animation: fadeSlideUp 0.6s ease-out; @@ -303,6 +329,20 @@ body { transform: translateY(0); } +.auth-btn.secondary { + background: rgba(255, 255, 255, 0.08); + color: var(--text-primary); + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: none; + margin-bottom: 12px; +} + +.auth-btn.secondary:hover { + background: rgba(255, 255, 255, 0.12); + border-color: var(--accent); + transform: translateY(-2px); +} + .btn-arrow { font-size: 18px; transition: transform 0.2s ease; @@ -336,6 +376,89 @@ body { text-decoration: underline; } +/* ==================== INFO PANEL (Right Side) ==================== */ +.auth-right { + animation: fadeSlideUp 0.6s ease-out 0.2s backwards; +} + +.info-card { + background: rgba(24, 24, 27, 0.5); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + padding: 28px; + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +.info-header h3 { + font-size: 1.125rem; + font-weight: 700; + color: var(--text-primary); + margin: 0 0 24px 0; +} + +.info-list { + display: flex; + flex-direction: column; + gap: 20px; +} + +.info-item { + display: flex; + gap: 14px; + align-items: start; +} + +.info-icon { + font-size: 28px; + line-height: 1; + flex-shrink: 0; +} + +.info-text strong { + display: block; + font-size: 0.9375rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; +} + +.info-text p { + font-size: 0.8125rem; + color: var(--text-secondary); + line-height: 1.4; + margin: 0; +} + +.info-footer { + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.status-badge { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.8125rem; + color: var(--text-secondary); +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--success); + box-shadow: 0 0 12px var(--success); + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + .auth-features { display: flex; flex-wrap: wrap; @@ -364,16 +487,61 @@ body { .desktop { width: 100%; height: calc(100vh - var(--taskbar-height)); - background: var(--bg-primary); - background-image: - radial-gradient(ellipse 100% 100% at 50% -30%, rgba(99, 102, 241, 0.08), transparent 70%), - radial-gradient(ellipse 80% 60% at 100% 100%, rgba(139, 92, 246, 0.05), transparent 60%); + background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%); position: relative; overflow: hidden; } +/* Animated astral background layers */ +.desktop::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: + radial-gradient(2px 2px at 20px 30px, rgba(255,255,255,0.9), transparent), + radial-gradient(2px 2px at 60px 70px, rgba(255,255,255,0.9), transparent), + radial-gradient(1px 1px at 50px 50px, rgba(255,255,255,0.8), transparent), + radial-gradient(1px 1px at 130px 80px, rgba(255,255,255,0.8), transparent), + radial-gradient(2px 2px at 90px 10px, rgba(255,255,255,0.9), transparent); + background-repeat: repeat; + background-size: 200px 200px; + animation: astralMove 60s linear infinite; + opacity: 0.5; + pointer-events: none; +} + +.desktop::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: + radial-gradient(ellipse 800px 600px at 30% 50%, rgba(99, 102, 241, 0.15), transparent), + radial-gradient(ellipse 600px 800px at 70% 50%, rgba(139, 92, 246, 0.1), transparent), + radial-gradient(ellipse 500px 500px at 50% 30%, rgba(236, 72, 153, 0.08), transparent); + animation: astralGlow 20s ease-in-out infinite; + pointer-events: none; +} + +@keyframes astralMove { + from { transform: translateY(0); } + to { transform: translateY(-200px); } +} + +@keyframes astralGlow { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 0.7; } +} + /* Desktop Icons */ .desktop-icons { + position: relative; + z-index: 1; display: flex; flex-direction: column; flex-wrap: wrap; @@ -1215,3 +1383,357 @@ body { background: linear-gradient(135deg, rgba(239, 68, 68, 0.9) 0%, rgba(220, 38, 38, 0.9) 100%); border-color: var(--danger); } + +/* ==================== TEXT EDITOR ==================== */ +.editor-toolbar { + display: flex; + gap: 8px; + padding: 12px; + background: rgba(0, 0, 0, 0.3); + border-bottom: 1px solid var(--border); +} + +.editor-filename-input { + flex: 1; + padding: 8px 12px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 14px; + font-family: inherit; +} + +.editor-filename-input:focus { + outline: none; + border-color: var(--accent); +} + +.editor-btn { + padding: 8px 16px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.editor-btn:hover { + background: rgba(255, 255, 255, 0.1); + border-color: var(--accent); +} + +.editor-textarea { + width: 100%; + height: 100%; + padding: 16px; + background: rgba(0, 0, 0, 0.2); + border: none; + color: var(--text-primary); + font-size: 14px; + font-family: 'Courier New', monospace; + resize: none; + outline: none; +} + +.editor-textarea::placeholder { + color: var(--text-muted); +} + +/* ==================== FILE MANAGER ==================== */ +.files-toolbar { + display: flex; + gap: 8px; + padding: 12px; + background: rgba(0, 0, 0, 0.3); + border-bottom: 1px solid var(--border); +} + +.files-btn { + padding: 8px 16px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.files-btn:hover { + background: rgba(255, 255, 255, 0.1); + border-color: var(--accent); +} + +.files-list { + padding: 16px; +} + +.file-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + margin-bottom: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.file-item:hover { + background: rgba(255, 255, 255, 0.05); + border-color: var(--accent); +} + +.file-item-info h4 { + font-size: 14px; + color: var(--text-primary); + margin-bottom: 4px; +} + +.file-item-info p { + font-size: 12px; + color: var(--text-secondary); +} + +.file-item-actions { + display: flex; + gap: 8px; +} + +.file-item-btn { + padding: 4px 8px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.file-item-btn:hover { + background: rgba(255, 255, 255, 0.1); +} + +/* ==================== CALCULATOR ==================== */ +.calculator { + padding: 20px; + max-width: 320px; + margin: 0 auto; +} + +.calculator-display { + width: 100%; + padding: 20px; + background: rgba(0, 0, 0, 0.4); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 32px; + font-weight: 600; + text-align: right; + margin-bottom: 16px; + min-height: 60px; + word-break: break-all; +} + +.calculator-buttons { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; +} + +.calc-btn { + padding: 20px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 18px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.calc-btn:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateY(-2px); +} + +.calc-btn:active { + transform: translateY(0); +} + +.calc-operator { + background: rgba(99, 102, 241, 0.2); +} + +.calc-operator:hover { + background: rgba(99, 102, 241, 0.3); +} + +.calc-equals { + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%); +} + +.calc-equals:hover { + box-shadow: 0 4px 16px rgba(99, 102, 241, 0.4); +} + +.calc-clear { + background: rgba(239, 68, 68, 0.2); +} + +.calc-clear:hover { + background: rgba(239, 68, 68, 0.3); +} + +/* ==================== NOTES APP ==================== */ +#notes-window .window-content { + display: flex; + padding: 0; +} + +.notes-sidebar { + width: 250px; + background: rgba(0, 0, 0, 0.3); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; +} + +.notes-btn-new { + margin: 12px; + padding: 12px; + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%); + border: none; + border-radius: var(--radius-sm); + color: white; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.notes-btn-new:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(99, 102, 241, 0.4); +} + +.notes-list { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.notes-item { + padding: 12px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + margin-bottom: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.notes-item:hover { + background: rgba(255, 255, 255, 0.05); + border-color: var(--accent); +} + +.notes-item.active { + background: rgba(99, 102, 241, 0.2); + border-color: var(--accent); +} + +.notes-item h4 { + font-size: 14px; + color: var(--text-primary); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.notes-item p { + font-size: 12px; + color: var(--text-secondary); +} + +.notes-editor { + flex: 1; + display: flex; + flex-direction: column; + padding: 16px; +} + +.notes-title-input { + width: 100%; + padding: 12px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 18px; + font-weight: 600; + margin-bottom: 12px; +} + +.notes-title-input:focus { + outline: none; + border-color: var(--accent); +} + +.notes-textarea { + flex: 1; + padding: 12px; + background: rgba(0, 0, 0, 0.2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 14px; + font-family: inherit; + resize: none; + margin-bottom: 12px; +} + +.notes-textarea:focus { + outline: none; + border-color: var(--accent); +} + +.notes-footer { + display: flex; + gap: 8px; +} + +.notes-btn { + padding: 10px 20px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.notes-btn:hover { + background: rgba(255, 255, 255, 0.1); + border-color: var(--accent); +} + +.notes-btn-danger { + background: rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.4); +} + +.notes-btn-danger:hover { + background: rgba(239, 68, 68, 0.3); +} diff --git a/server.js b/server.js index be39589..3a429bb 100644 --- a/server.js +++ b/server.js @@ -154,6 +154,11 @@ const apiLimiter = rateLimit({ app.use(express.static('public')); +// Serve xterm and socket.io from node_modules +app.use('/xterm', express.static(path.join(__dirname, 'node_modules/xterm'))); +app.use('/xterm-addon-fit', express.static(path.join(__dirname, 'node_modules/xterm-addon-fit'))); +app.use('/socket.io', express.static(path.join(__dirname, 'node_modules/socket.io/client-dist'))); + // Authentication middleware function requireAuth(req, res, next) { if (req.session.userId) {