Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MySecureNotes | Personal Information Hub</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- PDF.js for document preview --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script> | |
| <!-- SortableJS for drag and drop --> | |
| <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js"></script> | |
| <style> | |
| .sidebar { | |
| transition: all 0.3s ease; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .sidebar-content { | |
| flex: 1; | |
| overflow-y: auto; | |
| scrollbar-width: thin; | |
| scrollbar-color: #4a5568 #2d3748; | |
| } | |
| .sidebar-content::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .sidebar-content::-webkit-scrollbar-track { | |
| background: #2d3748; | |
| } | |
| .sidebar-content::-webkit-scrollbar-thumb { | |
| background-color: #4a5568; | |
| border-radius: 4px; | |
| } | |
| .note-content { | |
| min-height: 70vh; | |
| } | |
| .file-upload { | |
| position: relative; | |
| overflow: hidden; | |
| display: inline-block; | |
| } | |
| .file-upload input[type=file] { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| opacity: 0; | |
| } | |
| .note-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| } | |
| #dropZone { | |
| border: 2px dashed #cbd5e0; | |
| transition: all 0.3s ease; | |
| } | |
| #dropZone.drag-over { | |
| border-color: #4299e1; | |
| background-color: #ebf8ff; | |
| } | |
| .document-preview { | |
| height: 200px; | |
| overflow: hidden; | |
| position: relative; | |
| background-color: #f5f5f5; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .document-preview img { | |
| max-width: 100%; | |
| max-height: 100%; | |
| object-fit: contain; | |
| } | |
| .document-preview canvas { | |
| max-width: 100%; | |
| max-height: 100%; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .document-preview .file-icon { | |
| font-size: 48px; | |
| color: #6b7280; | |
| } | |
| .document-preview .preview-text { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: rgba(0,0,0,0.7); | |
| color: white; | |
| padding: 5px; | |
| font-size: 12px; | |
| text-align: center; | |
| } | |
| .document-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); | |
| gap: 20px; | |
| } | |
| .document-card { | |
| background: white; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| } | |
| .document-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); | |
| } | |
| .document-info { | |
| padding: 12px; | |
| } | |
| .document-actions { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 8px 12px; | |
| border-top: 1px solid #eee; | |
| } | |
| .sortable-ghost { | |
| opacity: 0.5; | |
| background: #c8ebfb; | |
| } | |
| .notebook { | |
| margin-left: 12px; | |
| border-left: 2px solid #e2e8f0; | |
| padding-left: 8px; | |
| } | |
| .notebook-header { | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| .notebook-header:hover { | |
| background-color: #f7fafc; | |
| } | |
| .note-page { | |
| margin-left: 24px; | |
| padding-left: 8px; | |
| border-left: 2px solid #e2e8f0; | |
| } | |
| .note-page:hover { | |
| background-color: #f7fafc; | |
| } | |
| .drag-handle { | |
| cursor: move; | |
| margin-right: 8px; | |
| color: #a0aec0; | |
| } | |
| .document-tags { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 4px; | |
| margin-top: 8px; | |
| } | |
| .document-tag { | |
| background-color: #ebf8ff; | |
| color: #3182ce; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| } | |
| .document-dropzone { | |
| height: 10px; | |
| background-color: #ebf8ff; | |
| margin: 5px 0; | |
| border-radius: 4px; | |
| transition: all 0.2s; | |
| } | |
| .document-dropzone.active { | |
| height: 30px; | |
| background-color: #bee3f8; | |
| } | |
| .edit-notebook-input { | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| width: 100%; | |
| font-weight: 600; | |
| } | |
| .document-in-notebook { | |
| background-color: #f0f9ff; | |
| border-left: 3px solid #3b82f6; | |
| } | |
| .active-notebook { | |
| background-color: rgba(59, 130, 246, 0.1); | |
| } | |
| .note-page.drag-over { | |
| background-color: #ebf8ff; | |
| border-left: 3px solid #3b82f6; | |
| } | |
| /* New styles for scrollable sections */ | |
| .directory-section { | |
| overflow-y: auto; | |
| max-height: 40vh; /* Adjust this value as needed */ | |
| scrollbar-width: thin; | |
| scrollbar-color: #4a5568 #2d3748; | |
| } | |
| .directory-section::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .directory-section::-webkit-scrollbar-track { | |
| background: #2d3748; | |
| } | |
| .directory-section::-webkit-scrollbar-thumb { | |
| background-color: #4a5568; | |
| border-radius: 4px; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 font-sans"> | |
| <!-- Login Modal --> | |
| <div id="loginModal" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"> | |
| <div class="bg-white rounded-lg shadow-xl p-8 w-full max-w-md"> | |
| <div class="text-center mb-6"> | |
| <h2 class="text-2xl font-bold text-gray-800">Welcome Back</h2> | |
| <p class="text-gray-600">Secure access to your personal notes</p> | |
| </div> | |
| <form id="loginForm" class="space-y-4"> | |
| <div> | |
| <label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email</label> | |
| <input type="email" id="email" name="email" required | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| <div> | |
| <label for="password" class="block text-sm font-medium text-gray-700 mb-1">Password</label> | |
| <input type="password" id="password" name="password" required | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="remember" name="remember" | |
| class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"> | |
| <label for="remember" class="ml-2 block text-sm text-gray-700">Remember me</label> | |
| </div> | |
| <a href="#" class="text-sm text-blue-600 hover:text-blue-500">Forgot password?</a> | |
| </div> | |
| <button type="submit" | |
| class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition"> | |
| Sign In | |
| </button> | |
| </form> | |
| <div class="mt-4 text-center"> | |
| <p class="text-sm text-gray-600">Don't have an account? <a href="#" class="text-blue-600 hover:text-blue-500">Register</a></p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main App (hidden until login) --> | |
| <div id="appContainer" class="hidden"> | |
| <!-- Sidebar --> | |
| <div class="sidebar fixed inset-y-0 left-0 bg-gray-800 text-white w-64 transform -translate-x-full md:translate-x-0 z-40"> | |
| <div class="flex items-center justify-between p-4 border-b border-gray-700"> | |
| <div class="flex items-center space-x-2"> | |
| <i class="fas fa-lock text-blue-400"></i> | |
| <span class="text-xl font-semibold">MySecureNotes</span> | |
| </div> | |
| <button id="sidebarToggle" class="md:hidden text-gray-400 hover:text-white"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="p-4"> | |
| <div class="flex items-center space-x-3 mb-6"> | |
| <div class="h-10 w-10 rounded-full bg-blue-500 flex items-center justify-center"> | |
| <span class="text-white font-medium" id="userInitial">U</span> | |
| </div> | |
| <div> | |
| <p class="font-medium" id="userName">User</p> | |
| <p class="text-xs text-gray-400">Premium Member</p> | |
| </div> | |
| </div> | |
| <div class="flex flex-col space-y-2 mb-6"> | |
| <button id="newNotebookBtn" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md flex items-center justify-center space-x-2"> | |
| <i class="fas fa-plus"></i> | |
| <span>New Notebook</span> | |
| </button> | |
| <button id="newNoteBtn" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md flex items-center justify-center space-x-2"> | |
| <i class="fas fa-plus"></i> | |
| <span>New Note</span> | |
| </button> | |
| </div> | |
| <div class="sidebar-content"> | |
| <div class="mb-4"> | |
| <div class="flex items-center justify-between text-gray-400 mb-2"> | |
| <span class="text-sm font-medium">MY NOTES</span> | |
| </div> | |
| <div class="directory-section"> | |
| <ul id="notesList" class="space-y-1"> | |
| <!-- Notes will be populated here --> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <div class="flex items-center justify-between text-gray-400 mb-2"> | |
| <span class="text-sm font-medium">DOCUMENTS</span> | |
| <i class="fas fa-chevron-down text-xs"></i> | |
| </div> | |
| <div class="directory-section"> | |
| <ul id="documentsList" class="space-y-1"> | |
| <!-- Documents will be populated here --> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="pt-4 border-t border-gray-700"> | |
| <button id="logoutBtn" class="flex items-center space-x-2 text-gray-400 hover:text-white w-full"> | |
| <i class="fas fa-sign-out-alt"></i> | |
| <span>Logout</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="md:ml-64"> | |
| <!-- Top Navigation --> | |
| <nav class="bg-white shadow-sm"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="flex justify-between h-16"> | |
| <div class="flex items-center md:hidden"> | |
| <button id="mobileSidebarToggle" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <div class="relative"> | |
| <button id="searchBtn" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-search"></i> | |
| </button> | |
| <div id="searchBox" class="hidden absolute right-0 mt-2 w-64 bg-white rounded-md shadow-lg z-10"> | |
| <input type="text" placeholder="Search notes..." class="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| </div> | |
| <button class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-bell"></i> | |
| </button> | |
| <div class="relative"> | |
| <button id="userMenuBtn" class="flex items-center space-x-2"> | |
| <div class="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center"> | |
| <span class="text-white text-sm font-medium" id="userInitialNav">U</span> | |
| </div> | |
| <span class="hidden md:inline text-sm font-medium">User</span> | |
| </button> | |
| <div id="userMenu" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-10"> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Profile</a> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Settings</a> | |
| <div class="border-t border-gray-200"></div> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Logout</a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Main Content Area --> | |
| <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> | |
| <div id="emptyState" class="flex flex-col items-center justify-center h-96"> | |
| <i class="fas fa-sticky-note text-gray-300 text-6xl mb-4"></i> | |
| <h3 class="text-lg font-medium text-gray-900">No note selected</h3> | |
| <p class="text-gray-500 mt-1">Select a note from the sidebar or create a new one</p> | |
| <button id="emptyNewNoteBtn" class="mt-4 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700"> | |
| Create New Note | |
| </button> | |
| </div> | |
| <div id="noteEditor" class="hidden bg-white rounded-lg shadow-sm overflow-hidden"> | |
| <div class="border-b border-gray-200 px-4 py-3 flex justify-between items-center"> | |
| <input type="text" id="noteTitle" placeholder="Note Title" class="text-xl font-medium w-full focus:outline-none"> | |
| <div class="flex space-x-2"> | |
| <button class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-share-alt"></i> | |
| </button> | |
| <button class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-ellipsis-v"></i> | |
| </button> | |
| <button id="deleteNoteBtn" class="text-red-500 hover:text-red-700"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="p-4"> | |
| <div id="noteContent" class="note-content min-h-[500px] focus:outline-none" contenteditable="true"> | |
| Start typing your note here... | |
| </div> | |
| <div class="mt-4 flex items-center justify-between border-t border-gray-200 pt-4"> | |
| <div class="text-sm text-gray-500"> | |
| Last saved: <span id="lastSaved">Just now</span> | |
| </div> | |
| <button id="saveNoteBtn" class="bg-blue-600 text-white py-1 px-3 rounded-md text-sm hover:bg-blue-700"> | |
| Save Changes | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="documentsView" class="hidden"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-xl font-semibold text-gray-800">My Documents</h2> | |
| <div class="flex space-x-2"> | |
| <div class="relative"> | |
| <select id="sortDocuments" class="appearance-none bg-white border border-gray-300 rounded-md px-3 py-2 pr-8 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <option value="name-asc">Sort by: Name (A-Z)</option> | |
| <option value="name-desc">Sort by: Name (Z-A)</option> | |
| <option value="date-new">Sort by: Date (Newest)</option> | |
| <option value="date-old">Sort by: Date (Oldest)</option> | |
| <option value="size-large">Sort by: Size (Large)</option> | |
| <option value="size-small">Sort by: Size (Small)</option> | |
| </select> | |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> | |
| <i class="fas fa-chevron-down text-xs"></i> | |
| </div> | |
| </div> | |
| <div class="file-upload"> | |
| <button class="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 flex items-center space-x-2"> | |
| <i class="fas fa-upload"></i> | |
| <span>Upload Document</span> | |
| </button> | |
| <input type="file" id="fileInput" class="hidden" multiple> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mb-4 flex items-center space-x-2"> | |
| <input type="text" id="documentSearch" placeholder="Search documents..." class="flex-1 px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <button id="addTagBtn" class="bg-gray-200 text-gray-700 py-2 px-3 rounded-md hover:bg-gray-300"> | |
| <i class="fas fa-tag"></i> | |
| </button> | |
| <div id="tagDropdown" class="hidden absolute right-0 mt-10 w-48 bg-white rounded-md shadow-lg z-10 p-2"> | |
| <div class="flex flex-wrap gap-2"> | |
| <span class="document-tag cursor-pointer" data-tag="work">work</span> | |
| <span class="document-tag cursor-pointer" data-tag="personal">personal</span> | |
| <span class="document-tag cursor-pointer" data-tag="important">important</span> | |
| <span class="document-tag cursor-pointer" data-tag="finance">finance</span> | |
| <span class="document-tag cursor-pointer" data-tag="archive">archive</span> | |
| </div> | |
| <input type="text" id="newTagInput" placeholder="Add new tag" class="mt-2 w-full px-2 py-1 text-sm border border-gray-300 rounded"> | |
| <button id="saveTagBtn" class="mt-2 w-full bg-blue-500 text-white py-1 px-2 rounded text-sm">Add</button> | |
| </div> | |
| </div> | |
| <div id="dropZone" class="border-2 border-dashed border-gray-300 rounded-lg p-12 text-center mb-6"> | |
| <i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-3"></i> | |
| <h3 class="text-lg font-medium text-gray-700">Drag and drop files here</h3> | |
| <p class="text-gray-500 mt-1">or click to browse files</p> | |
| </div> | |
| <div class="document-grid" id="documentsGrid"> | |
| <!-- Documents will be listed here as cards --> | |
| <div class="text-center py-8 text-gray-500 col-span-full"> | |
| <i class="fas fa-folder-open text-3xl mb-2"></i> | |
| <p>No documents uploaded yet</p> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| <script> | |
| // Set PDF.js worker path | |
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js'; | |
| // Sample data for demonstration | |
| const sampleNotebooks = [ | |
| { | |
| id: 1, | |
| title: 'Work', | |
| isOpen: true, | |
| pages: [ | |
| { id: 101, title: 'Project Ideas', content: '1. Personal finance tracker\n2. Recipe organizer app\n3. Travel journal website', lastUpdated: '2023-06-10T14:45:00' }, | |
| { id: 102, title: 'Meeting Notes', content: 'Discussed new features\n- User authentication\n- Dashboard design\n- API endpoints', lastUpdated: '2023-06-12T09:30:00' } | |
| ], | |
| documents: [] | |
| }, | |
| { | |
| id: 2, | |
| title: 'Personal', | |
| isOpen: false, | |
| pages: [ | |
| { id: 201, title: 'Shopping List', content: '- Milk\n- Eggs\n- Bread\n- Fruits\n- Vegetables', lastUpdated: '2023-06-05T09:15:00' }, | |
| { id: 202, title: 'Books to Read', content: '1. Atomic Habits\n2. Deep Work\n3. The Psychology of Money', lastUpdated: '2023-05-28T16:20:00' } | |
| ], | |
| documents: [] | |
| } | |
| ]; | |
| // Sample documents with preview images (in a real app, these would be generated from actual files) | |
| const sampleDocuments = [ | |
| { | |
| id: 1, | |
| name: 'Resume.pdf', | |
| type: 'PDF', | |
| size: '2.4 MB', | |
| date: '2023-05-20', | |
| tags: ['work', 'important'], | |
| previewUrl: 'https://example.com/previews/resume.jpg', // This would be generated in a real app | |
| notebookId: null // Track which notebook this document belongs to | |
| }, | |
| { | |
| id: 2, | |
| name: 'Contract.docx', | |
| type: 'Word', | |
| size: '1.1 MB', | |
| date: '2023-04-15', | |
| tags: ['work', 'finance'], | |
| previewUrl: null, // No preview for non-PDF files | |
| notebookId: 1 // Belongs to Work notebook | |
| }, | |
| { | |
| id: 3, | |
| name: 'Budget.xlsx', | |
| type: 'Excel', | |
| size: '3.2 MB', | |
| date: '2023-03-10', | |
| tags: ['personal', 'finance'], | |
| previewUrl: null, // No preview for non-PDF files | |
| notebookId: 2 // Belongs to Personal notebook | |
| } | |
| ]; | |
| // DOM Elements | |
| const loginModal = document.getElementById('loginModal'); | |
| const appContainer = document.getElementById('appContainer'); | |
| const loginForm = document.getElementById('loginForm'); | |
| const logoutBtn = document.getElementById('logoutBtn'); | |
| const sidebarToggle = document.getElementById('sidebarToggle'); | |
| const mobileSidebarToggle = document.getElementById('mobileSidebarToggle'); | |
| const sidebar = document.querySelector('.sidebar'); | |
| const notesList = document.getElementById('notesList'); | |
| const documentsList = document.getElementById('documentsList'); | |
| const newNoteBtn = document.getElementById('newNoteBtn'); | |
| const newNotebookBtn = document.getElementById('newNotebookBtn'); | |
| const emptyNewNoteBtn = document.getElementById('emptyNewNoteBtn'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const noteEditor = document.getElementById('noteEditor'); | |
| const noteTitle = document.getElementById('noteTitle'); | |
| const noteContent = document.getElementById('noteContent'); | |
| const saveNoteBtn = document.getElementById('saveNoteBtn'); | |
| const deleteNoteBtn = document.getElementById('deleteNoteBtn'); | |
| const documentsView = document.getElementById('documentsView'); | |
| const documentsGrid = document.getElementById('documentsGrid'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const dropZone = document.getElementById('dropZone'); | |
| const userInitial = document.getElementById('userInitial'); | |
| const userInitialNav = document.getElementById('userInitialNav'); | |
| const userName = document.getElementById('userName'); | |
| const userMenuBtn = document.getElementById('userMenuBtn'); | |
| const userMenu = document.getElementById('userMenu'); | |
| const searchBtn = document.getElementById('searchBtn'); | |
| const searchBox = document.getElementById('searchBox'); | |
| const lastSaved = document.getElementById('lastSaved'); | |
| const sortDocuments = document.getElementById('sortDocuments'); | |
| const documentSearch = document.getElementById('documentSearch'); | |
| const addTagBtn = document.getElementById('addTagBtn'); | |
| const tagDropdown = document.getElementById('tagDropdown'); | |
| const newTagInput = document.getElementById('newTagInput'); | |
| const saveTagBtn = document.getElementById('saveTagBtn'); | |
| // Current state | |
| let currentNote = null; | |
| let currentNotebook = null; | |
| let currentView = 'notes'; // 'notes' or 'documents' | |
| let notebooks = [...sampleNotebooks]; | |
| let documents = [...sampleDocuments]; | |
| let autoSaveTimer = null; | |
| let uploadedFiles = {}; // Store uploaded files for download | |
| let currentSort = 'name-asc'; | |
| let currentTags = []; | |
| let allTags = ['work', 'personal', 'important', 'finance', 'archive']; | |
| let editingNotebookId = null; | |
| let lastActiveNotebook = null; // Track the last active notebook | |
| // Initialize the app | |
| function initApp() { | |
| // For demo purposes, show login modal by default | |
| loginModal.classList.remove('hidden'); | |
| // Populate notes and documents lists | |
| renderNotebooksList(); | |
| renderDocumentsList(); | |
| // Set up event listeners | |
| setupEventListeners(); | |
| // Initialize Sortable for document grid | |
| new Sortable(documentsGrid, { | |
| animation: 150, | |
| ghostClass: 'sortable-ghost', | |
| onEnd: function(evt) { | |
| // Update documents order in our state | |
| const items = Array.from(documentsGrid.children); | |
| const newOrder = items.map(item => parseInt(item.querySelector('.download-btn').getAttribute('data-id'))); | |
| // Reorder documents array | |
| documents.sort((a, b) => newOrder.indexOf(a.id) - newOrder.indexOf(b.id)); | |
| } | |
| }); | |
| } | |
| // Set up all event listeners | |
| function setupEventListeners() { | |
| // Login form submission | |
| loginForm.addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const email = document.getElementById('email').value; | |
| const password = document.getElementById('password').value; | |
| // In a real app, you would validate credentials with a server | |
| // For demo, we'll just accept any non-empty credentials | |
| if (email && password) { | |
| loginModal.classList.add('hidden'); | |
| appContainer.classList.remove('hidden'); | |
| // Set user info (in a real app, this would come from the server) | |
| const name = email.split('@')[0]; | |
| userInitial.textContent = name.charAt(0).toUpperCase(); | |
| userInitialNav.textContent = name.charAt(0).toUpperCase(); | |
| userName.textContent = name; | |
| } | |
| }); | |
| // Logout button | |
| logoutBtn.addEventListener('click', function() { | |
| appContainer.classList.add('hidden'); | |
| loginModal.classList.remove('hidden'); | |
| resetAppState(); | |
| }); | |
| // Sidebar toggle for mobile | |
| sidebarToggle.addEventListener('click', function() { | |
| sidebar.classList.add('-translate-x-full'); | |
| }); | |
| mobileSidebarToggle.addEventListener('click', function() { | |
| sidebar.classList.remove('-translate-x-full'); | |
| }); | |
| // New note buttons | |
| newNoteBtn.addEventListener('click', createNewNote); | |
| emptyNewNoteBtn.addEventListener('click', createNewNote); | |
| newNotebookBtn.addEventListener('click', createNewNotebook); | |
| // Note editor events | |
| noteTitle.addEventListener('input', startAutoSave); | |
| noteContent.addEventListener('input', startAutoSave); | |
| saveNoteBtn.addEventListener('click', saveCurrentNote); | |
| deleteNoteBtn.addEventListener('click', deleteCurrentNote); | |
| // File upload | |
| fileInput.addEventListener('change', handleFileUpload); | |
| // Drag and drop | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| ['dragenter', 'dragoover'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, unhighlight, false); | |
| }); | |
| dropZone.addEventListener('drop', handleDrop, false); | |
| // User menu | |
| userMenuBtn.addEventListener('click', function() { | |
| userMenu.classList.toggle('hidden'); | |
| }); | |
| // Search | |
| searchBtn.addEventListener('click', function() { | |
| searchBox.classList.toggle('hidden'); | |
| }); | |
| // Document sorting | |
| sortDocuments.addEventListener('change', function() { | |
| currentSort = this.value; | |
| renderDocumentsGrid(); | |
| }); | |
| // Document search | |
| documentSearch.addEventListener('input', function() { | |
| renderDocumentsGrid(); | |
| }); | |
| // Tag management | |
| addTagBtn.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| tagDropdown.classList.toggle('hidden'); | |
| }); | |
| document.addEventListener('click', function(e) { | |
| if (!addTagBtn.contains(e.target) && !tagDropdown.contains(e.target)) { | |
| tagDropdown.classList.add('hidden'); | |
| } | |
| }); | |
| // Tag selection | |
| tagDropdown.addEventListener('click', function(e) { | |
| if (e.target.classList.contains('document-tag') && e.target.getAttribute('data-tag')) { | |
| const tag = e.target.getAttribute('data-tag'); | |
| if (!currentTags.includes(tag)) { | |
| currentTags.push(tag); | |
| renderDocumentsGrid(); | |
| } | |
| } | |
| }); | |
| // Add new tag | |
| saveTagBtn.addEventListener('click', function() { | |
| const newTag = newTagInput.value.trim().toLowerCase(); | |
| if (newTag && !allTags.includes(newTag)) { | |
| allTags.push(newTag); | |
| newTagInput.value = ''; | |
| renderTagDropdown(); | |
| } | |
| }); | |
| // Close menus when clicking elsewhere | |
| document.addEventListener('click', function(e) { | |
| if (!userMenuBtn.contains(e.target) && !userMenu.contains(e.target)) { | |
| userMenu.classList.add('hidden'); | |
| } | |
| if (!searchBtn.contains(e.target) && !searchBox.contains(e.target)) { | |
| searchBox.classList.add('hidden'); | |
| } | |
| }); | |
| } | |
| // Prevent default drag and drop behavior | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| // Highlight drop zone | |
| function highlight() { | |
| dropZone.classList.add('drag-over'); | |
| } | |
| // Unhighlight drop zone | |
| function unhighlight() { | |
| dropZone.classList.remove('drag-over'); | |
| } | |
| // Handle dropped files | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| handleFileUpload({ target: { files } }); | |
| } | |
| // Handle file upload | |
| function handleFileUpload(e) { | |
| const files = e.target.files; | |
| if (files.length > 0) { | |
| // In a real app, you would upload files to a server | |
| // For demo, we'll just add them to our documents array | |
| Array.from(files).forEach(file => { | |
| const newDoc = { | |
| id: Date.now(), // Use timestamp as unique ID | |
| name: file.name, | |
| type: file.type || getFileType(file.name), | |
| size: formatFileSize(file.size), | |
| date: new Date().toISOString().split('T')[0], | |
| tags: currentTags.length > 0 ? [...currentTags] : [], | |
| previewUrl: null, | |
| file: file, // Store the actual file for download | |
| notebookId: null // Initially not in any notebook | |
| }; | |
| // Store the file for download | |
| uploadedFiles[newDoc.id] = file; | |
| // Generate preview for PDF files | |
| if (file.type === 'application/pdf') { | |
| generatePdfPreview(file).then(previewUrl => { | |
| newDoc.previewUrl = previewUrl; | |
| renderDocumentsGrid(); | |
| }); | |
| } | |
| documents.unshift(newDoc); | |
| currentTags = []; // Reset current tags after upload | |
| }); | |
| renderDocumentsList(); | |
| renderDocumentsGrid(); | |
| // Show success message | |
| alert(`${files.length} file(s) uploaded successfully!`); | |
| } | |
| } | |
| // Get file type from extension | |
| function getFileType(filename) { | |
| const extension = filename.split('.').pop().toLowerCase(); | |
| const types = { | |
| 'pdf': 'PDF', | |
| 'doc': 'Word', | |
| 'docx': 'Word', | |
| 'xls': 'Excel', | |
| 'xlsx': 'Excel', | |
| 'jpg': 'Image', | |
| 'jpeg': 'Image', | |
| 'png': 'Image', | |
| 'gif': 'Image' | |
| }; | |
| return types[extension] || 'File'; | |
| } | |
| // Generate PDF preview | |
| async function generatePdfPreview(file) { | |
| return new Promise((resolve) => { | |
| const reader = new FileReader(); | |
| reader.onload = async function(e) { | |
| try { | |
| const typedArray = new Uint8Array(e.target.result); | |
| const pdf = await pdfjsLib.getDocument(typedArray).promise; | |
| const page = await pdf.getPage(1); | |
| const viewport = page.getViewport({ scale: 0.5 }); | |
| const canvas = document.createElement('canvas'); | |
| const context = canvas.getContext('2d'); | |
| canvas.height = viewport.height; | |
| canvas.width = viewport.width; | |
| await page.render({ | |
| canvasContext: context, | |
| viewport: viewport | |
| }).promise; | |
| resolve(canvas.toDataURL('image/jpeg')); | |
| } catch (error) { | |
| console.error('Error generating PDF preview:', error); | |
| resolve(null); | |
| } | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| }); | |
| } | |
| // Format file size | |
| function formatFileSize(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| // Create a new note | |
| function createNewNote() { | |
| // Determine which notebook to add to | |
| let targetNotebook = null; | |
| // If we have a current notebook, use that | |
| if (currentNotebook) { | |
| targetNotebook = notebooks.find(nb => nb.id === currentNotebook); | |
| } | |
| // Otherwise use the last active notebook | |
| else if (lastActiveNotebook) { | |
| targetNotebook = notebooks.find(nb => nb.id === lastActiveNotebook); | |
| } | |
| // Otherwise create a new notebook | |
| else if (notebooks.length > 0) { | |
| targetNotebook = notebooks[0]; // Default to first notebook | |
| } else { | |
| // No notebooks exist, create one | |
| const newNotebook = { | |
| id: Date.now() + 1, | |
| title: 'New Notebook', | |
| isOpen: true, | |
| pages: [], | |
| documents: [] | |
| }; | |
| notebooks.unshift(newNotebook); | |
| targetNotebook = newNotebook; | |
| } | |
| const newNote = { | |
| id: Date.now(), // Use timestamp as unique ID | |
| title: 'Untitled Note', | |
| content: '', | |
| lastUpdated: new Date().toISOString() | |
| }; | |
| if (targetNotebook) { | |
| targetNotebook.pages.unshift(newNote); | |
| lastActiveNotebook = targetNotebook.id; // Update last active notebook | |
| // Make sure notebook is open | |
| targetNotebook.isOpen = true; | |
| renderNotebooksList(); | |
| openNote(newNote.id); | |
| // Focus on title field | |
| setTimeout(() => { | |
| noteTitle.focus(); | |
| }, 100); | |
| } | |
| } | |
| // Create a new notebook | |
| function createNewNotebook() { | |
| const newNotebook = { | |
| id: Date.now(), | |
| title: 'New Notebook', | |
| isOpen: true, | |
| pages: [], | |
| documents: [] | |
| }; | |
| notebooks.unshift(newNotebook); | |
| renderNotebooksList(); | |
| // Start editing the notebook name immediately | |
| startEditingNotebookName(newNotebook.id); | |
| } | |
| // Start editing notebook name | |
| function startEditingNotebookName(notebookId) { | |
| editingNotebookId = notebookId; | |
| renderNotebooksList(); | |
| // Focus on the input field | |
| setTimeout(() => { | |
| const input = document.querySelector(`.notebook-header[data-id="${notebookId}"] .edit-notebook-input`); | |
| if (input) { | |
| input.focus(); | |
| input.select(); | |
| } | |
| }, 100); | |
| } | |
| // Save notebook name | |
| function saveNotebookName(notebookId, newName) { | |
| const notebook = notebooks.find(nb => nb.id === notebookId); | |
| if (notebook && newName.trim()) { | |
| notebook.title = newName.trim(); | |
| editingNotebookId = null; | |
| renderNotebooksList(); | |
| } | |
| } | |
| // Open a note by ID | |
| function openNote(noteId) { | |
| // Find the note in any notebook | |
| for (const notebook of notebooks) { | |
| const note = notebook.pages.find(page => page.id === noteId); | |
| if (note) { | |
| currentNote = note; | |
| currentNotebook = notebook.id; | |
| lastActiveNotebook = notebook.id; // Update last active notebook | |
| // Open the notebook if it's closed | |
| if (!notebook.isOpen) { | |
| notebook.isOpen = true; | |
| renderNotebooksList(); | |
| } | |
| emptyState.classList.add('hidden'); | |
| noteEditor.classList.remove('hidden'); | |
| documentsView.classList.add('hidden'); | |
| currentView = 'notes'; | |
| noteTitle.value = currentNote.title; | |
| noteContent.innerHTML = currentNote.content.replace(/\n/g, '<br>'); | |
| lastSaved.textContent = formatDate(currentNote.lastUpdated); | |
| return; | |
| } | |
| } | |
| } | |
| // Toggle notebook open/closed | |
| function toggleNotebook(notebookId) { | |
| const notebook = notebooks.find(nb => nb.id === notebookId); | |
| if (notebook && editingNotebookId !== notebookId) { | |
| notebook.isOpen = !notebook.isOpen; | |
| lastActiveNotebook = notebookId; // Update last active notebook | |
| renderNotebooksList(); | |
| } | |
| } | |
| // Start auto-save timer | |
| function startAutoSave() { | |
| if (autoSaveTimer) { | |
| clearTimeout(autoSaveTimer); | |
| } | |
| autoSaveTimer = setTimeout(() => { | |
| saveCurrentNote(); | |
| }, 2000); | |
| } | |
| // Save current note | |
| function saveCurrentNote() { | |
| if (currentNote) { | |
| currentNote.title = noteTitle.value; | |
| currentNote.content = noteContent.innerHTML.replace(/<br>/g, '\n'); | |
| currentNote.lastUpdated = new Date().toISOString(); | |
| // Update last saved time | |
| lastSaved.textContent = 'Just now'; | |
| // Update the note in the sidebar list | |
| renderNotebooksList(); | |
| } | |
| } | |
| // Delete current note | |
| function deleteCurrentNote() { | |
| if (currentNote && confirm('Are you sure you want to delete this note?')) { | |
| // Find and remove the note from its notebook | |
| for (const notebook of notebooks) { | |
| const noteIndex = notebook.pages.findIndex(page => page.id === currentNote.id); | |
| if (noteIndex !== -1) { | |
| notebook.pages.splice(noteIndex, 1); | |
| // If notebook is now empty, delete it too | |
| if (notebook.pages.length === 0 && notebook.documents.length === 0) { | |
| const notebookIndex = notebooks.findIndex(nb => nb.id === notebook.id); | |
| if (notebookIndex !== -1) { | |
| notebooks.splice(notebookIndex, 1); | |
| } | |
| } | |
| renderNotebooksList(); | |
| closeNoteEditor(); | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| // Close note editor | |
| function closeNoteEditor() { | |
| emptyState.classList.remove('hidden'); | |
| noteEditor.classList.add('hidden'); | |
| currentNote = null; | |
| currentNotebook = null; | |
| } | |
| // Render notebooks and notes in sidebar | |
| function renderNotebooksList() { | |
| notesList.innerHTML = ''; | |
| if (notebooks.length === 0) { | |
| notesList.innerHTML = ` | |
| <li class="text-gray-400 text-sm px-3 py-2"> | |
| No notebooks yet. Create one to get started. | |
| </li> | |
| `; | |
| return; | |
| } | |
| notebooks.forEach(notebook => { | |
| const notebookItem = document.createElement('li'); | |
| notebookItem.className = 'notebook'; | |
| // Notebook header | |
| const notebookHeader = document.createElement('div'); | |
| notebookHeader.className = `notebook-header flex items-center justify-between px-2 py-1 rounded-md ${notebook.id === lastActiveNotebook ? 'active-notebook' : ''}`; | |
| notebookHeader.setAttribute('data-id', notebook.id); | |
| if (editingNotebookId === notebook.id) { | |
| notebookHeader.innerHTML = ` | |
| <div class="flex items-center w-full"> | |
| <i class="fas fa-folder-open text-yellow-400 mr-2"></i> | |
| <input type="text" value="${notebook.title}" | |
| class="edit-notebook-input" | |
| onblur="saveNotebookName(${notebook.id}, this.value)" | |
| onkeydown="if(event.key === 'Enter') { this.blur(); }"> | |
| </div> | |
| `; | |
| } else { | |
| notebookHeader.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i class="fas ${notebook.isOpen ? 'fa-folder-open' : 'fa-folder'} text-yellow-400 mr-2"></i> | |
| <span class="truncate">${notebook.title}</span> | |
| </div> | |
| <div class="flex items-center"> | |
| <span class="text-xs text-gray-400 mr-2">${notebook.pages.length + notebook.documents.length}</span> | |
| <i class="fas fa-chevron-${notebook.isOpen ? 'down' : 'right'} text-xs text-gray-400"></i> | |
| </div> | |
| `; | |
| // Add double click to edit notebook name | |
| notebookHeader.addEventListener('dblclick', (e) => { | |
| if (!e.target.classList.contains('fa-ellipsis-v')) { | |
| startEditingNotebookName(notebook.id); | |
| } | |
| }); | |
| } | |
| notebookHeader.addEventListener('click', () => toggleNotebook(notebook.id)); | |
| notebookItem.appendChild(notebookHeader); | |
| // Notebook pages (notes) and documents | |
| if (notebook.isOpen) { | |
| const contentList = document.createElement('ul'); | |
| contentList.className = 'mt-1'; | |
| // Make notebook content sortable | |
| new Sortable(contentList, { | |
| animation: 150, | |
| ghostClass: 'sortable-ghost', | |
| handle: '.drag-handle', | |
| onEnd: function(evt) { | |
| // Update order in our state | |
| const items = Array.from(contentList.children); | |
| // Separate notes and documents | |
| const newNoteOrder = []; | |
| const newDocOrder = []; | |
| items.forEach(item => { | |
| const id = parseInt(item.getAttribute('data-id')); | |
| if (item.classList.contains('note-page')) { | |
| newNoteOrder.push(id); | |
| } else if (item.classList.contains('document-in-notebook')) { | |
| newDocOrder.push(id); | |
| } | |
| }); | |
| // Reorder notebook pages array | |
| notebook.pages.sort((a, b) => newNoteOrder.indexOf(a.id) - newNoteOrder.indexOf(b.id)); | |
| // Reorder notebook documents array | |
| notebook.documents.sort((a, b) => newDocOrder.indexOf(a.id) - newDocOrder.indexOf(b.id)); | |
| } | |
| }); | |
| // Add drop zones between items | |
| function addDropZone(container, position) { | |
| const dropZone = document.createElement('li'); | |
| dropZone.className = 'document-dropzone'; | |
| dropZone.setAttribute('data-dropzone', position); | |
| dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.add('active'); | |
| }); | |
| dropZone.addEventListener('dragleave', () => { | |
| dropZone.classList.remove('active'); | |
| }); | |
| dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('active'); | |
| const docId = e.dataTransfer.getData('text/plain'); | |
| if (docId) { | |
| moveDocumentToNotebook(parseInt(docId), notebook.id, position); | |
| } | |
| }); | |
| container.appendChild(dropZone); | |
| } | |
| // Initial drop zone at top | |
| addDropZone(contentList, 'top'); | |
| // Add notes | |
| notebook.pages.forEach(page => { | |
| const pageItem = document.createElement('li'); | |
| pageItem.className = 'note-page flex items-center px-2 py-1 rounded-md'; | |
| pageItem.setAttribute('data-id', page.id); | |
| pageItem.innerHTML = ` | |
| <i class="fas fa-grip-lines drag-handle mr-2"></i> | |
| <i class="fas fa-file-alt text-blue-400 mr-2"></i> | |
| <span class="truncate flex-1">${page.title}</span> | |
| <span class="text-xs text-gray-400">${formatDate(page.lastUpdated)}</span> | |
| `; | |
| // Add drag and drop events for linking documents | |
| pageItem.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| pageItem.classList.add('drag-over'); | |
| }); | |
| pageItem.addEventListener('dragleave', () => { | |
| pageItem.classList.remove('drag-over'); | |
| }); | |
| pageItem.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| pageItem.classList.remove('drag-over'); | |
| const docId = e.dataTransfer.getData('text/plain'); | |
| if (docId) { | |
| linkDocumentToNote(parseInt(docId), page.id); | |
| } | |
| }); | |
| pageItem.addEventListener('click', (e) => { | |
| // Don't open note if clicking on drag handle | |
| if (!e.target.classList.contains('drag-handle') && !e.target.classList.contains('fa-grip-lines')) { | |
| openNote(page.id); | |
| } | |
| }); | |
| // Make draggable | |
| pageItem.draggable = true; | |
| pageItem.addEventListener('dragstart', (e) => { | |
| e.dataTransfer.setData('text/plain', page.id); | |
| e.dataTransfer.effectAllowed = 'move'; | |
| }); | |
| contentList.appendChild(pageItem); | |
| // Add drop zone after each note | |
| addDropZone(contentList, page.id); | |
| }); | |
| // Add documents | |
| notebook.documents.forEach(doc => { | |
| const docItem = document.createElement('li'); | |
| docItem.className = 'document-in-notebook flex items-center px-2 py-1 rounded-md'; | |
| docItem.setAttribute('data-id', doc.id); | |
| docItem.innerHTML = ` | |
| <i class="fas fa-grip-lines drag-handle mr-2"></i> | |
| <i class="fas ${getFileIcon(doc.type)} text-blue-400 mr-2"></i> | |
| <span class="truncate flex-1">${doc.name}</span> | |
| <button class="remove-doc-btn text-red-500 hover:text-red-700 text-xs"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| `; | |
| // Make draggable | |
| docItem.draggable = true; | |
| docItem.addEventListener('dragstart', (e) => { | |
| e.dataTransfer.setData('text/plain', doc.id); | |
| e.dataTransfer.effectAllowed = 'move'; | |
| }); | |
| // Add click to open document | |
| docItem.addEventListener('click', (e) => { | |
| if (!e.target.classList.contains('remove-doc-btn') && | |
| !e.target.classList.contains('fa-times') && | |
| !e.target.classList.contains('drag-handle') && | |
| !e.target.classList.contains('fa-grip-lines')) { | |
| showDocumentsView(); | |
| } | |
| }); | |
| // Add remove button | |
| const removeBtn = docItem.querySelector('.remove-doc-btn'); | |
| removeBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| removeDocumentFromNotebook(doc.id, notebook.id); | |
| }); | |
| contentList.appendChild(docItem); | |
| // Add drop zone after each document | |
| addDropZone(contentList, doc.id); | |
| }); | |
| notebookItem.appendChild(contentList); | |
| } | |
| notesList.appendChild(notebookItem); | |
| }); | |
| } | |
| // Link document to note (create a reference/link) | |
| function linkDocumentToNote(docId, noteId) { | |
| const doc = documents.find(d => d.id === docId); | |
| if (!doc) return; | |
| const note = findNoteById(noteId); | |
| if (!note) return; | |
| // In a real app, you would create a proper link/reference | |
| // For demo, we'll just add a link to the note content | |
| const docLink = `[${doc.name}](${docId})`; | |
| // If note is currently open, update the editor | |
| if (currentNote && currentNote.id === noteId) { | |
| noteContent.innerHTML += `<p>${docLink}</p>`; | |
| saveCurrentNote(); | |
| } else { | |
| // Otherwise update the note content directly | |
| note.content += `\n${docLink}`; | |
| note.lastUpdated = new Date().toISOString(); | |
| renderNotebooksList(); | |
| } | |
| alert(`Document "${doc.name}" linked to note "${note.title}"`); | |
| } | |
| // Find note by ID | |
| function findNoteById(noteId) { | |
| for (const notebook of notebooks) { | |
| const note = notebook.pages.find(page => page.id === noteId); | |
| if (note) return note; | |
| } | |
| return null; | |
| } | |
| // Move document to notebook at specific position | |
| function moveDocumentToNotebook(docId, notebookId, position) { | |
| // Find the document | |
| const doc = documents.find(d => d.id === docId); | |
| if (!doc) return; | |
| // Remove from current notebook if it's in one | |
| if (doc.notebookId) { | |
| const currentNotebook = notebooks.find(nb => nb.id === doc.notebookId); | |
| if (currentNotebook) { | |
| const docIndex = currentNotebook.documents.findIndex(d => d.id === docId); | |
| if (docIndex !== -1) { | |
| currentNotebook.documents.splice(docIndex, 1); | |
| } | |
| } | |
| } | |
| // Add to new notebook | |
| const notebook = notebooks.find(nb => nb.id === notebookId); | |
| if (notebook) { | |
| // Update document's notebook reference | |
| doc.notebookId = notebookId; | |
| // Find position to insert | |
| if (position === 'top') { | |
| notebook.documents.unshift(doc); | |
| } else { | |
| // Find the item (note or document) with this ID | |
| const itemIndex = notebook.pages.findIndex(p => p.id === position); | |
| if (itemIndex !== -1) { | |
| // Insert after this note | |
| notebook.documents.splice(itemIndex + 1, 0, doc); | |
| } else { | |
| // Maybe it's after a document | |
| const docIndex = notebook.documents.findIndex(d => d.id === position); | |
| if (docIndex !== -1) { | |
| notebook.documents.splice(docIndex + 1, 0, doc); | |
| } else { | |
| // Default to adding at the end | |
| notebook.documents.push(doc); | |
| } | |
| } | |
| } | |
| renderNotebooksList(); | |
| renderDocumentsGrid(); | |
| } | |
| } | |
| // Remove document from notebook | |
| function removeDocumentFromNotebook(docId, notebookId) { | |
| const notebook = notebooks.find(nb => nb.id === notebookId); | |
| if (notebook) { | |
| const docIndex = notebook.documents.findIndex(d => d.id === docId); | |
| if (docIndex !== -1) { | |
| notebook.documents.splice(docIndex, 1); | |
| // Update document's notebook reference | |
| const doc = documents.find(d => d.id === docId); | |
| if (doc) { | |
| doc.notebookId = null; | |
| } | |
| renderNotebooksList(); | |
| renderDocumentsGrid(); | |
| } | |
| } | |
| } | |
| // Render documents list in sidebar | |
| function renderDocumentsList() { | |
| documentsList.innerHTML = ''; | |
| documents.slice(0, 3).forEach(doc => { | |
| const docItem = document.createElement('li'); | |
| docItem.className = 'bg-gray-700 hover:bg-gray-600 rounded-md px-3 py-2 cursor-pointer'; | |
| docItem.innerHTML = ` | |
| <div class="flex items-center space-x-2"> | |
| <i class="fas ${getFileIcon(doc.type)} text-blue-400"></i> | |
| <span class="truncate">${doc.name}</span> | |
| </div> | |
| `; | |
| docItem.addEventListener('click', () => showDocumentsView()); | |
| documentsList.appendChild(docItem); | |
| }); | |
| if (documents.length > 3) { | |
| const moreItem = document.createElement('li'); | |
| moreItem.className = 'text-gray-400 text-sm px-3 py-1'; | |
| moreItem.textContent = `+${documents.length - 3} more...`; | |
| moreItem.addEventListener('click', () => showDocumentsView()); | |
| documentsList.appendChild(moreItem); | |
| } | |
| } | |
| // Get appropriate file icon | |
| function getFileIcon(fileType) { | |
| const icons = { | |
| 'PDF': 'fa-file-pdf', | |
| 'Word': 'fa-file-word', | |
| 'Excel': 'fa-file-excel', | |
| 'Image': 'fa-file-image' | |
| }; | |
| return icons[fileType] || 'fa-file'; | |
| } | |
| // Show documents view | |
| function showDocumentsView() { | |
| emptyState.classList.add('hidden'); | |
| noteEditor.classList.add('hidden'); | |
| documentsView.classList.remove('hidden'); | |
| currentView = 'documents'; | |
| renderDocumentsGrid(); | |
| } | |
| // Sort documents based on current sort option | |
| function sortDocumentsList(docs) { | |
| const searchTerm = documentSearch.value.toLowerCase(); | |
| // Filter by search term if provided | |
| let filteredDocs = docs; | |
| if (searchTerm) { | |
| filteredDocs = docs.filter(doc => | |
| doc.name.toLowerCase().includes(searchTerm) || | |
| (doc.tags && doc.tags.some(tag => tag.includes(searchTerm))) | |
| ); | |
| } | |
| // Filter by tags if any are selected | |
| if (currentTags.length > 0) { | |
| filteredDocs = filteredDocs.filter(doc => | |
| doc.tags && currentTags.every(tag => doc.tags.includes(tag)) | |
| ); | |
| } | |
| // Apply sorting | |
| return filteredDocs.sort((a, b) => { | |
| switch (currentSort) { | |
| case 'name-asc': | |
| return a.name.localeCompare(b.name); | |
| case 'name-desc': | |
| return b.name.localeCompare(a.name); | |
| case 'date-new': | |
| return new Date(b.date) - new Date(a.date); | |
| case 'date-old': | |
| return new Date(a.date) - new Date(b.date); | |
| case 'size-large': | |
| return parseFloat(b.size) - parseFloat(a.size); | |
| case 'size-small': | |
| return parseFloat(a.size) - parseFloat(b.size); | |
| default: | |
| return 0; | |
| } | |
| }); | |
| } | |
| // Render documents grid as cards with previews | |
| function renderDocumentsGrid() { | |
| documentsGrid.innerHTML = ''; | |
| const sortedDocuments = sortDocumentsList(documents); | |
| if (sortedDocuments.length === 0) { | |
| documentsGrid.innerHTML = ` | |
| <div class="text-center py-8 text-gray-500 col-span-full"> | |
| <i class="fas fa-folder-open text-3xl mb-2"></i> | |
| <p>No documents found</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| sortedDocuments.forEach(doc => { | |
| const docCard = document.createElement('div'); | |
| docCard.className = `document-card ${doc.notebookId ? 'border-2 border-blue-400' : ''}`; | |
| docCard.setAttribute('draggable', 'true'); | |
| docCard.setAttribute('data-id', doc.id); | |
| // Preview section | |
| let previewContent = ''; | |
| if (doc.previewUrl) { | |
| previewContent = `<img src="${doc.previewUrl}" alt="${doc.name} preview" class="w-full h-full object-cover">`; | |
| } else { | |
| const icon = getFileIcon(doc.type); | |
| previewContent = ` | |
| <div class="flex flex-col items-center justify-center h-full"> | |
| <i class="fas ${icon} text-4xl text-gray-500"></i> | |
| <p class="mt-2 text-sm text-gray-500">No preview available</p> | |
| </div> | |
| `; | |
| } | |
| // Tags section | |
| let tagsContent = ''; | |
| if (doc.tags && doc.tags.length > 0) { | |
| tagsContent = ` | |
| <div class="document-tags"> | |
| ${doc.tags.map(tag => `<span class="document-tag">${tag}</span>`).join('')} | |
| </div> | |
| `; | |
| } | |
| // Notebook info if in a notebook | |
| let notebookInfo = ''; | |
| if (doc.notebookId) { | |
| const notebook = notebooks.find(nb => nb.id === doc.notebookId); | |
| if (notebook) { | |
| notebookInfo = ` | |
| <div class="flex items-center text-xs text-blue-600 mt-1"> | |
| <i class="fas fa-folder mr-1"></i> | |
| <span class="truncate">${notebook.title}</span> | |
| </div> | |
| `; | |
| } | |
| } | |
| docCard.innerHTML = ` | |
| <div class="document-preview"> | |
| ${previewContent} | |
| <div class="preview-text">${doc.name}</div> | |
| </div> | |
| <div class="document-info"> | |
| <h3 class="font-medium truncate">${doc.name}</h3> | |
| <div class="flex justify-between text-sm text-gray-500 mt-1"> | |
| <span>${doc.type}</span> | |
| <span>${doc.size}</span> | |
| </div> | |
| ${notebookInfo} | |
| ${tagsContent} | |
| </div> | |
| <div class="document-actions"> | |
| <span class="text-xs text-gray-500">${doc.date}</span> | |
| <div class="flex space-x-2"> | |
| <button class="delete-doc-btn text-red-600 hover:text-red-800" data-id="${doc.id}"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| <button class="download-btn text-blue-600 hover:text-blue-800" data-id="${doc.id}"> | |
| <i class="fas fa-download"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| // Add drag events | |
| docCard.addEventListener('dragstart', (e) => { | |
| e.dataTransfer.setData('text/plain', doc.id); | |
| e.dataTransfer.effectAllowed = 'move'; | |
| }); | |
| documentsGrid.appendChild(docCard); | |
| }); | |
| // Add event listeners to download buttons | |
| document.querySelectorAll('.download-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const docId = parseInt(btn.getAttribute('data-id')); | |
| downloadDocument(docId); | |
| }); | |
| }); | |
| // Add event listeners to delete buttons | |
| document.querySelectorAll('.delete-doc-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const docId = parseInt(btn.getAttribute('data-id')); | |
| deleteDocument(docId); | |
| }); | |
| }); | |
| } | |
| // Render tag dropdown | |
| function renderTagDropdown() { | |
| const tagContainer = tagDropdown.querySelector('.flex-wrap'); | |
| tagContainer.innerHTML = ''; | |
| allTags.forEach(tag => { | |
| const tagElement = document.createElement('span'); | |
| tagElement.className = 'document-tag cursor-pointer'; | |
| tagElement.setAttribute('data-tag', tag); | |
| tagElement.textContent = tag; | |
| tagContainer.appendChild(tagElement); | |
| }); | |
| } | |
| // Download document | |
| function downloadDocument(docId) { | |
| const doc = documents.find(d => d.id === docId); | |
| if (!doc) return; | |
| // Check if we have the actual file (for uploaded files) | |
| if (uploadedFiles[docId]) { | |
| const file = uploadedFiles[docId]; | |
| const url = URL.createObjectURL(file); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = file.name; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } else { | |
| // For demo files, we can't actually download them | |
| alert(`Downloading ${doc.name}`); | |
| } | |
| } | |
| // Delete document | |
| function deleteDocument(docId) { | |
| if (confirm('Are you sure you want to delete this document?')) { | |
| // Remove from documents array | |
| documents = documents.filter(doc => doc.id !== docId); | |
| // Remove from any notebook it might be in | |
| notebooks.forEach(notebook => { | |
| notebook.documents = notebook.documents.filter(doc => doc.id !== docId); | |
| }); | |
| // Remove from uploaded files if it exists there | |
| if (uploadedFiles[docId]) { | |
| delete uploadedFiles[docId]; | |
| } | |
| renderDocumentsList(); | |
| renderDocumentsGrid(); | |
| renderNotebooksList(); | |
| } | |
| } | |
| // Format date for display | |
| function formatDate(dateString) { | |
| const date = new Date(dateString); | |
| const now = new Date(); | |
| const diffInDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); | |
| if (diffInDays === 0) { | |
| return 'Today'; | |
| } else if (diffInDays === 1) { | |
| return 'Yesterday'; | |
| } else if (diffInDays < 7) { | |
| return `${diffInDays} days ago`; | |
| } else { | |
| return date.toLocaleDateString(); | |
| } | |
| } | |
| // Reset app state | |
| function resetAppState() { | |
| currentNote = null; | |
| currentNotebook = null; | |
| emptyState.classList.remove('hidden'); | |
| noteEditor.classList.add('hidden'); | |
| documentsView.classList.add('hidden'); | |
| } | |
| // Initialize the app when the page loads | |
| window.addEventListener('DOMContentLoaded', initApp); | |
| // Global functions for inline event handlers | |
| window.saveNotebookName = saveNotebookName; | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=lwmecham/useful-space" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |