Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DocClone - Google Docs Clone</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/docx/7.1.0/docx.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .editor { | |
| min-height: calc(100vh - 120px); | |
| } | |
| .document-item:hover { | |
| background-color: #f3f4f6; | |
| } | |
| .document-item.active { | |
| background-color: #e5e7eb; | |
| } | |
| #sidebar { | |
| transition: all 0.3s ease; | |
| } | |
| @media (max-width: 768px) { | |
| #sidebar { | |
| position: fixed; | |
| left: -100%; | |
| top: 0; | |
| z-index: 50; | |
| height: 100vh; | |
| width: 80%; | |
| } | |
| #sidebar.open { | |
| left: 0; | |
| } | |
| #overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(0,0,0,0.5); | |
| z-index: 40; | |
| } | |
| #overlay.open { | |
| display: block; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <!-- Header --> | |
| <header class="bg-white shadow-sm"> | |
| <div class="flex items-center justify-between px-4 py-2"> | |
| <div class="flex items-center space-x-4"> | |
| <button id="menu-btn" class="md:hidden text-gray-600"> | |
| <i class="fas fa-bars text-xl"></i> | |
| </button> | |
| <div class="flex items-center"> | |
| <i class="fas fa-file-word text-blue-500 text-2xl mr-2"></i> | |
| <h1 class="text-xl font-bold text-gray-800">DocClone</h1> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button id="save-btn" class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition"> | |
| <i class="fas fa-save mr-1"></i> Save | |
| </button> | |
| <button id="export-btn" class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition"> | |
| <i class="fas fa-file-export mr-1"></i> Export DOCX | |
| </button> | |
| <div class="relative"> | |
| <button id="new-doc-btn" class="px-3 py-1 bg-purple-500 text-white rounded hover:bg-purple-600 transition"> | |
| <i class="fas fa-plus mr-1"></i> New | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="flex"> | |
| <!-- Sidebar --> | |
| <div id="sidebar" class="bg-white w-64 h-screen shadow-md overflow-y-auto"> | |
| <div class="p-4 border-b"> | |
| <h2 class="text-lg font-semibold text-gray-700">My Documents</h2> | |
| </div> | |
| <div id="documents-list" class="p-2"> | |
| <!-- Documents will be loaded here --> | |
| </div> | |
| </div> | |
| <!-- Overlay for mobile --> | |
| <div id="overlay" class=""></div> | |
| <!-- Editor --> | |
| <div class="flex-1"> | |
| <div class="bg-white shadow-sm p-4"> | |
| <div class="flex items-center space-x-4 overflow-x-auto"> | |
| <select id="font-family" class="px-2 py-1 border rounded"> | |
| <option value="Arial">Arial</option> | |
| <option value="Times New Roman">Times New Roman</option> | |
| <option value="Courier New">Courier New</option> | |
| <option value="Georgia">Georgia</option> | |
| <option value="Verdana">Verdana</option> | |
| </select> | |
| <select id="font-size" class="px-2 py-1 border rounded"> | |
| <option value="1">8pt</option> | |
| <option value="2">10pt</option> | |
| <option value="3">12pt</option> | |
| <option value="4">14pt</option> | |
| <option value="5">18pt</option> | |
| <option value="6">24pt</option> | |
| <option value="7">36pt</option> | |
| </select> | |
| <button id="bold-btn" class="p-1 rounded hover:bg-gray-100"> | |
| <i class="fas fa-bold"></i> | |
| </button> | |
| <button id="italic-btn" class="p-1 rounded hover:bg-gray-100"> | |
| <i class="fas fa-italic"></i> | |
| </button> | |
| <button id="underline-btn" class="p-1 rounded hover:bg-gray-100"> | |
| <i class="fas fa-underline"></i> | |
| </button> | |
| <div class="border-l h-6 mx-2"></div> | |
| <button id="align-left-btn" class="p-1 rounded hover:bg-gray-100"> | |
| <i class="fas fa-align-left"></i> | |
| </button> | |
| <button id="align-center-btn" class="p-1 rounded hover:bg-gray-100"> | |
| <i class="fas fa-align-center"></i> | |
| </button> | |
| <button id="align-right-btn" class="p-1 rounded hover:bg-gray-100"> | |
| <i class="fas fa-align-right"></i> | |
| </button> | |
| <div class="border-l h-6 mx-2"></div> | |
| <button id="list-ul-btn" class="p-1 rounded hover:bg-gray-100"> | |
| <i class="fas fa-list-ul"></i> | |
| </button> | |
| <button id="list-ol-btn" class="p-1 rounded hover:bg-gray-100"> | |
| <i class="fas fa-list-ol"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="p-4"> | |
| <div id="editor" class="editor bg-white border rounded p-6 shadow-inner focus:outline-none" contenteditable="true"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Document Name Modal --> | |
| <div id="doc-name-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50"> | |
| <div class="bg-white rounded-lg p-6 w-96"> | |
| <h3 class="text-lg font-semibold mb-4">Name your document</h3> | |
| <input type="text" id="doc-name-input" class="w-full px-3 py-2 border rounded mb-4" placeholder="Document name"> | |
| <div class="flex justify-end space-x-2"> | |
| <button id="cancel-doc-btn" class="px-4 py-2 border rounded hover:bg-gray-100">Cancel</button> | |
| <button id="confirm-doc-btn" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Create</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const editor = document.getElementById('editor'); | |
| const documentsList = document.getElementById('documents-list'); | |
| const saveBtn = document.getElementById('save-btn'); | |
| const exportBtn = document.getElementById('export-btn'); | |
| const newDocBtn = document.getElementById('new-doc-btn'); | |
| const menuBtn = document.getElementById('menu-btn'); | |
| const sidebar = document.getElementById('sidebar'); | |
| const overlay = document.getElementById('overlay'); | |
| const docNameModal = document.getElementById('doc-name-modal'); | |
| const docNameInput = document.getElementById('doc-name-input'); | |
| const confirmDocBtn = document.getElementById('confirm-doc-btn'); | |
| const cancelDocBtn = document.getElementById('cancel-doc-btn'); | |
| // Formatting buttons | |
| const boldBtn = document.getElementById('bold-btn'); | |
| const italicBtn = document.getElementById('italic-btn'); | |
| const underlineBtn = document.getElementById('underline-btn'); | |
| const alignLeftBtn = document.getElementById('align-left-btn'); | |
| const alignCenterBtn = document.getElementById('align-center-btn'); | |
| const alignRightBtn = document.getElementById('align-right-btn'); | |
| const listUlBtn = document.getElementById('list-ul-btn'); | |
| const listOlBtn = document.getElementById('list-ol-btn'); | |
| const fontFamily = document.getElementById('font-family'); | |
| const fontSize = document.getElementById('font-size'); | |
| // State | |
| let currentDocId = null; | |
| let documents = []; | |
| // Initialize | |
| loadDocuments(); | |
| if (documents.length > 0) { | |
| loadDocument(documents[0].id); | |
| } else { | |
| createNewDocument(); | |
| } | |
| // Event Listeners | |
| menuBtn.addEventListener('click', toggleSidebar); | |
| overlay.addEventListener('click', toggleSidebar); | |
| saveBtn.addEventListener('click', saveCurrentDocument); | |
| exportBtn.addEventListener('click', exportToDocx); | |
| newDocBtn.addEventListener('click', showNewDocModal); | |
| confirmDocBtn.addEventListener('click', createNewDocumentWithName); | |
| cancelDocBtn.addEventListener('click', hideNewDocModal); | |
| // Formatting event listeners | |
| boldBtn.addEventListener('click', () => document.execCommand('bold', false, null)); | |
| italicBtn.addEventListener('click', () => document.execCommand('italic', false, null)); | |
| underlineBtn.addEventListener('click', () => document.execCommand('underline', false, null)); | |
| alignLeftBtn.addEventListener('click', () => document.execCommand('justifyLeft', false, null)); | |
| alignCenterBtn.addEventListener('click', () => document.execCommand('justifyCenter', false, null)); | |
| alignRightBtn.addEventListener('click', () => document.execCommand('justifyRight', false, null)); | |
| listUlBtn.addEventListener('click', () => document.execCommand('insertUnorderedList', false, null)); | |
| listOlBtn.addEventListener('click', () => document.execCommand('insertOrderedList', false, null)); | |
| fontFamily.addEventListener('change', () => { | |
| document.execCommand('fontName', false, fontFamily.value); | |
| }); | |
| fontSize.addEventListener('change', () => { | |
| const sizes = ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt']; | |
| document.execCommand('fontSize', false, fontSize.value); | |
| // Fix the actual size (execCommand only sets the font size tag) | |
| const selection = window.getSelection(); | |
| if (selection.rangeCount > 0) { | |
| const range = selection.getRangeAt(0); | |
| const span = document.createElement('span'); | |
| span.style.fontSize = sizes[fontSize.value - 1]; | |
| range.surroundContents(span); | |
| } | |
| }); | |
| // Functions | |
| function toggleSidebar() { | |
| sidebar.classList.toggle('open'); | |
| overlay.classList.toggle('open'); | |
| } | |
| function showNewDocModal() { | |
| docNameModal.classList.remove('hidden'); | |
| docNameInput.focus(); | |
| } | |
| function hideNewDocModal() { | |
| docNameModal.classList.add('hidden'); | |
| docNameInput.value = ''; | |
| } | |
| function createNewDocumentWithName() { | |
| const name = docNameInput.value.trim() || 'Untitled Document'; | |
| createNewDocument(name); | |
| hideNewDocModal(); | |
| } | |
| function createNewDocument(name = 'Untitled Document') { | |
| const newDoc = { | |
| id: Date.now().toString(), | |
| name: name, | |
| content: '<p><br></p>', | |
| createdAt: new Date().toISOString(), | |
| updatedAt: new Date().toISOString() | |
| }; | |
| documents.unshift(newDoc); | |
| saveDocumentsToCookies(); | |
| renderDocumentsList(); | |
| loadDocument(newDoc.id); | |
| } | |
| function loadDocuments() { | |
| const docsCookie = getCookie('documents'); | |
| if (docsCookie) { | |
| documents = JSON.parse(docsCookie); | |
| renderDocumentsList(); | |
| } | |
| } | |
| function saveDocumentsToCookies() { | |
| setCookie('documents', JSON.stringify(documents), 365); | |
| } | |
| function renderDocumentsList() { | |
| documentsList.innerHTML = ''; | |
| if (documents.length === 0) { | |
| documentsList.innerHTML = '<p class="text-gray-500 p-2">No documents yet</p>'; | |
| return; | |
| } | |
| documents.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); | |
| documents.forEach(doc => { | |
| const docElement = document.createElement('div'); | |
| docElement.className = `document-item p-3 cursor-pointer rounded flex items-center justify-between ${currentDocId === doc.id ? 'active' : ''}`; | |
| docElement.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i class="fas fa-file-word text-blue-500 mr-2"></i> | |
| <span class="truncate">${doc.name}</span> | |
| </div> | |
| <button class="delete-doc text-red-500 hover:text-red-700 p-1" data-id="${doc.id}"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| `; | |
| docElement.addEventListener('click', () => loadDocument(doc.id)); | |
| const deleteBtn = docElement.querySelector('.delete-doc'); | |
| deleteBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| deleteDocument(doc.id); | |
| }); | |
| documentsList.appendChild(docElement); | |
| }); | |
| } | |
| function loadDocument(docId) { | |
| const doc = documents.find(d => d.id === docId); | |
| if (doc) { | |
| currentDocId = docId; | |
| editor.innerHTML = doc.content; | |
| renderDocumentsList(); | |
| // Set focus to editor | |
| editor.focus(); | |
| // Move cursor to end | |
| const range = document.createRange(); | |
| range.selectNodeContents(editor); | |
| range.collapse(false); | |
| const selection = window.getSelection(); | |
| selection.removeAllRanges(); | |
| selection.addRange(range); | |
| } | |
| } | |
| function saveCurrentDocument() { | |
| if (!currentDocId) return; | |
| const docIndex = documents.findIndex(d => d.id === currentDocId); | |
| if (docIndex !== -1) { | |
| documents[docIndex].content = editor.innerHTML; | |
| documents[docIndex].updatedAt = new Date().toISOString(); | |
| saveDocumentsToCookies(); | |
| renderDocumentsList(); | |
| // Show save notification | |
| showNotification('Document saved successfully'); | |
| } | |
| } | |
| function deleteDocument(docId) { | |
| if (confirm('Are you sure you want to delete this document?')) { | |
| documents = documents.filter(d => d.id !== docId); | |
| saveDocumentsToCookies(); | |
| if (currentDocId === docId) { | |
| if (documents.length > 0) { | |
| loadDocument(documents[0].id); | |
| } else { | |
| createNewDocument(); | |
| } | |
| } else { | |
| renderDocumentsList(); | |
| } | |
| } | |
| } | |
| function exportToDocx() { | |
| if (!currentDocId) return; | |
| const doc = documents.find(d => d.id === currentDocId); | |
| if (!doc) return; | |
| // Create a temporary div to parse the HTML content | |
| const tempDiv = document.createElement('div'); | |
| tempDiv.innerHTML = doc.content; | |
| // Initialize DOCX | |
| const { Document, Paragraph, TextRun, HeadingLevel, AlignmentType } = docx; | |
| const children = []; | |
| // Process each node in the content | |
| const nodes = tempDiv.childNodes; | |
| for (let i = 0; i < nodes.length; i++) { | |
| const node = nodes[i]; | |
| if (node.nodeType === Node.TEXT_NODE) { | |
| if (node.textContent.trim() !== '') { | |
| children.push( | |
| new Paragraph({ | |
| children: [new TextRun(node.textContent)], | |
| }) | |
| ); | |
| } | |
| } else if (node.nodeType === Node.ELEMENT_NODE) { | |
| if (node.tagName === 'P') { | |
| const paragraphChildren = []; | |
| const textRuns = []; | |
| // Process child nodes of the paragraph | |
| processNode(node, textRuns); | |
| if (textRuns.length > 0) { | |
| paragraphChildren.push(...textRuns); | |
| } | |
| children.push( | |
| new Paragraph({ | |
| children: paragraphChildren, | |
| }) | |
| ); | |
| } else if (node.tagName === 'H1') { | |
| children.push( | |
| new Paragraph({ | |
| text: node.textContent, | |
| heading: HeadingLevel.HEADING_1, | |
| }) | |
| ); | |
| } else if (node.tagName === 'H2') { | |
| children.push( | |
| new Paragraph({ | |
| text: node.textContent, | |
| heading: HeadingLevel.HEADING_2, | |
| }) | |
| ); | |
| } else if (node.tagName === 'UL' || node.tagName === 'OL') { | |
| const listItems = node.querySelectorAll('li'); | |
| listItems.forEach(li => { | |
| children.push( | |
| new Paragraph({ | |
| text: li.textContent, | |
| bullet: { | |
| level: 0 | |
| }, | |
| }) | |
| ); | |
| }); | |
| } else if (node.tagName === 'DIV') { | |
| // Handle divs (often used for line breaks) | |
| if (node.textContent.trim() !== '') { | |
| children.push( | |
| new Paragraph({ | |
| children: [new TextRun(node.textContent)], | |
| }) | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| // Create the document | |
| const docxDoc = new Document({ | |
| title: doc.name, | |
| description: "Exported from DocClone", | |
| creator: "DocClone", | |
| children: children, | |
| }); | |
| // Generate and download the DOCX file | |
| docx.Packer.toBlob(docxDoc).then(blob => { | |
| saveAs(blob, `${doc.name}.docx`); | |
| }); | |
| function processNode(element, textRuns) { | |
| for (let j = 0; j < element.childNodes.length; j++) { | |
| const child = element.childNodes[j]; | |
| if (child.nodeType === Node.TEXT_NODE) { | |
| if (child.textContent.trim() !== '') { | |
| textRuns.push( | |
| new TextRun({ | |
| text: child.textContent, | |
| bold: element.style.fontWeight === 'bold' || element.tagName === 'STRONG' || element.tagName === 'B', | |
| italics: element.style.fontStyle === 'italic' || element.tagName === 'EM' || element.tagName === 'I', | |
| underline: element.style.textDecoration === 'underline' || element.tagName === 'U', | |
| size: element.style.fontSize ? parseInt(element.style.fontSize) * 2 : undefined, | |
| font: element.style.fontFamily || undefined, | |
| }) | |
| ); | |
| } | |
| } else if (child.nodeType === Node.ELEMENT_NODE) { | |
| // Recursively process child elements | |
| processNode(child, textRuns); | |
| } | |
| } | |
| } | |
| } | |
| function showNotification(message) { | |
| const notification = document.createElement('div'); | |
| notification.className = 'fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded shadow-lg'; | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| setTimeout(() => { | |
| notification.classList.add('opacity-0', 'transition-opacity', 'duration-300'); | |
| setTimeout(() => notification.remove(), 300); | |
| }, 3000); | |
| } | |
| // Cookie helper functions | |
| function setCookie(name, value, days) { | |
| let expires = ""; | |
| if (days) { | |
| const date = new Date(); | |
| date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); | |
| expires = "; expires=" + date.toUTCString(); | |
| } | |
| document.cookie = name + "=" + (value || "") + expires + "; path=/"; | |
| } | |
| function getCookie(name) { | |
| const nameEQ = name + "="; | |
| const ca = document.cookie.split(';'); | |
| for (let i = 0; i < ca.length; i++) { | |
| let c = ca[i]; | |
| while (c.charAt(0) === ' ') c = c.substring(1, c.length); | |
| if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); | |
| } | |
| return null; | |
| } | |
| // Auto-save functionality | |
| let saveTimeout; | |
| editor.addEventListener('input', () => { | |
| clearTimeout(saveTimeout); | |
| saveTimeout = setTimeout(() => { | |
| saveCurrentDocument(); | |
| }, 2000); | |
| }); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| // Ctrl+S or Cmd+S to save | |
| if ((e.ctrlKey || e.metaKey) && e.key === 's') { | |
| e.preventDefault(); | |
| saveCurrentDocument(); | |
| } | |
| // Ctrl+N or Cmd+N to create new document | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'n') { | |
| e.preventDefault(); | |
| showNewDocModal(); | |
| } | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |