Spaces:
Running
Running
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Noted - Simple Note Taking App</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"> | |
| <style> | |
| /* Custom scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| } | |
| .dark ::-webkit-scrollbar-track { | |
| background: #1f2937; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #9ca3af; | |
| border-radius: 4px; | |
| } | |
| .dark ::-webkit-scrollbar-thumb { | |
| background: #4b5563; | |
| } | |
| /* Animation for notes */ | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .note-animation { | |
| animation: fadeIn 0.3s ease-out forwards; | |
| } | |
| /* Custom checkbox */ | |
| .custom-checkbox { | |
| appearance: none; | |
| -webkit-appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border: 2px solid #d1d5db; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| position: relative; | |
| transition: all 0.2s; | |
| } | |
| .custom-checkbox:checked { | |
| background-color: #3b82f6; | |
| border-color: #3b82f6; | |
| } | |
| .custom-checkbox:checked::after { | |
| content: "✓"; | |
| position: absolute; | |
| color: white; | |
| font-size: 12px; | |
| left: 4px; | |
| top: 1px; | |
| } | |
| .dark .custom-checkbox { | |
| border-color: #4b5563; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 dark:bg-gray-900 transition-colors duration-200 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <!-- Header --> | |
| <header class="flex justify-between items-center mb-8"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-sticky-note text-blue-500 text-3xl mr-3"></i> | |
| <h1 class="text-3xl font-bold text-gray-800 dark:text-white">Noted</h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button id="themeToggle" class="p-2 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"> | |
| <i class="fas fa-moon dark:hidden"></i> | |
| <i class="fas fa-sun hidden dark:inline"></i> | |
| </button> | |
| <button id="clearAllBtn" class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors hidden"> | |
| <i class="fas fa-trash mr-2"></i>Clear All | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Search and Add Note --> | |
| <div class="flex flex-col md:flex-row gap-4 mb-8"> | |
| <div class="relative flex-grow"> | |
| <input type="text" id="searchInput" placeholder="Search notes..." class="w-full px-4 py-3 pl-10 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-white"> | |
| <i class="fas fa-search absolute left-3 top-3.5 text-gray-400"></i> | |
| </div> | |
| <button id="addNoteBtn" class="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors font-medium flex items-center justify-center"> | |
| <i class="fas fa-plus mr-2"></i> Add Note | |
| </button> | |
| </div> | |
| <!-- Notes Grid --> | |
| <div id="notesContainer" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5"> | |
| <!-- Notes will be added here dynamically --> | |
| </div> | |
| <!-- Empty State --> | |
| <div id="emptyState" class="text-center py-16"> | |
| <i class="fas fa-sticky-note text-gray-300 dark:text-gray-700 text-6xl mb-4"></i> | |
| <h3 class="text-xl font-medium text-gray-500 dark:text-gray-400 mb-2">No notes yet</h3> | |
| <p class="text-gray-400 dark:text-gray-500 mb-4">Click "Add Note" to create your first note</p> | |
| </div> | |
| </div> | |
| <!-- Note Editor Modal --> | |
| <div id="noteEditorModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 hidden"> | |
| <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col"> | |
| <div class="flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700"> | |
| <h3 class="text-lg font-medium text-gray-800 dark:text-white" id="editorTitle">New Note</h3> | |
| <button id="closeEditorBtn" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"> | |
| <i class="fas fa-times text-xl"></i> | |
| </button> | |
| </div> | |
| <div class="p-4 flex-grow overflow-y-auto"> | |
| <input type="text" id="noteTitleInput" placeholder="Title" class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-white mb-4 font-medium"> | |
| <textarea id="noteContentInput" placeholder="Write your note here..." class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-white min-h-[200px]"></textarea> | |
| <div class="mt-4 flex items-center"> | |
| <input type="checkbox" id="notePinnedInput" class="custom-checkbox mr-2"> | |
| <label for="notePinnedInput" class="text-gray-700 dark:text-gray-300 cursor-pointer">Pin this note</label> | |
| </div> | |
| </div> | |
| <div class="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3"> | |
| <button id="cancelNoteBtn" class="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"> | |
| Cancel | |
| </button> | |
| <button id="saveNoteBtn" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"> | |
| Save | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Confirmation Modal --> | |
| <div id="confirmModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 hidden"> | |
| <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md"> | |
| <div class="p-6"> | |
| <div class="flex items-center mb-4"> | |
| <i class="fas fa-exclamation-triangle text-yellow-500 text-2xl mr-3"></i> | |
| <h3 class="text-lg font-medium text-gray-800 dark:text-white" id="confirmTitle">Confirm Action</h3> | |
| </div> | |
| <p class="text-gray-600 dark:text-gray-400 mb-6" id="confirmMessage">Are you sure you want to perform this action?</p> | |
| <div class="flex justify-end space-x-3"> | |
| <button id="cancelConfirmBtn" class="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"> | |
| Cancel | |
| </button> | |
| <button id="confirmActionBtn" class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"> | |
| Confirm | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const notesContainer = document.getElementById('notesContainer'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const addNoteBtn = document.getElementById('addNoteBtn'); | |
| const clearAllBtn = document.getElementById('clearAllBtn'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const themeToggle = document.getElementById('themeToggle'); | |
| // Modal Elements | |
| const noteEditorModal = document.getElementById('noteEditorModal'); | |
| const editorTitle = document.getElementById('editorTitle'); | |
| const noteTitleInput = document.getElementById('noteTitleInput'); | |
| const noteContentInput = document.getElementById('noteContentInput'); | |
| const notePinnedInput = document.getElementById('notePinnedInput'); | |
| const closeEditorBtn = document.getElementById('closeEditorBtn'); | |
| const cancelNoteBtn = document.getElementById('cancelNoteBtn'); | |
| const saveNoteBtn = document.getElementById('saveNoteBtn'); | |
| // Confirmation Modal Elements | |
| const confirmModal = document.getElementById('confirmModal'); | |
| const confirmTitle = document.getElementById('confirmTitle'); | |
| const confirmMessage = document.getElementById('confirmMessage'); | |
| const cancelConfirmBtn = document.getElementById('cancelConfirmBtn'); | |
| const confirmActionBtn = document.getElementById('confirmActionBtn'); | |
| // App State | |
| let notes = JSON.parse(localStorage.getItem('notes')) || []; | |
| let currentNoteId = null; | |
| let actionToConfirm = null; | |
| let searchTerm = ''; | |
| // Initialize the app | |
| function init() { | |
| renderNotes(); | |
| updateEmptyState(); | |
| updateClearAllButton(); | |
| // Check for saved theme preference | |
| const savedTheme = localStorage.getItem('theme'); | |
| if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { | |
| document.documentElement.classList.add('dark'); | |
| } | |
| } | |
| // Render notes to the UI | |
| function renderNotes() { | |
| notesContainer.innerHTML = ''; | |
| // Filter notes based on search term | |
| const filteredNotes = notes.filter(note => | |
| note.title.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| note.content.toLowerCase().includes(searchTerm.toLowerCase()) | |
| ); | |
| // Separate pinned and unpinned notes | |
| const pinnedNotes = filteredNotes.filter(note => note.pinned); | |
| const unpinnedNotes = filteredNotes.filter(note => !note.pinned); | |
| // Render pinned notes first | |
| if (pinnedNotes.length > 0) { | |
| const pinnedSection = document.createElement('div'); | |
| pinnedSection.className = 'col-span-full mb-2'; | |
| pinnedSection.innerHTML = ` | |
| <h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2 flex items-center"> | |
| <i class="fas fa-thumbtack mr-2"></i> Pinned Notes | |
| </h3> | |
| `; | |
| notesContainer.appendChild(pinnedSection); | |
| pinnedNotes.forEach(note => { | |
| notesContainer.appendChild(createNoteElement(note)); | |
| }); | |
| // Add divider if there are both pinned and unpinned notes | |
| if (unpinnedNotes.length > 0) { | |
| const divider = document.createElement('div'); | |
| divider.className = 'col-span-full border-t border-gray-200 dark:border-gray-700 my-4'; | |
| notesContainer.appendChild(divider); | |
| } | |
| } | |
| // Render unpinned notes | |
| unpinnedNotes.forEach(note => { | |
| notesContainer.appendChild(createNoteElement(note)); | |
| }); | |
| } | |
| // Create a note element for the DOM | |
| function createNoteElement(note) { | |
| const noteElement = document.createElement('div'); | |
| noteElement.className = `note-animation bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden border border-gray-200 dark:border-gray-700 flex flex-col h-full ${note.pinned ? 'border-l-4 border-l-blue-500' : ''}`; | |
| noteElement.dataset.id = note.id; | |
| // Format the date | |
| const date = new Date(note.updatedAt); | |
| const formattedDate = date.toLocaleDateString('en-US', { | |
| month: 'short', | |
| day: 'numeric', | |
| year: 'numeric' | |
| }); | |
| noteElement.innerHTML = ` | |
| <div class="p-4 flex-grow"> | |
| <div class="flex justify-between items-start mb-2"> | |
| <h3 class="font-medium text-gray-800 dark:text-white truncate">${note.title}</h3> | |
| <button class="pin-btn text-gray-400 hover:text-blue-500 ml-2" data-id="${note.id}"> | |
| <i class="fas ${note.pinned ? 'fa-thumbtack text-blue-500' : 'fa-thumbtack'}"></i> | |
| </button> | |
| </div> | |
| <p class="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-3">${note.content}</p> | |
| <div class="text-xs text-gray-400 dark:text-gray-500">${formattedDate}</div> | |
| </div> | |
| <div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-2"> | |
| <button class="edit-btn px-3 py-1 text-sm text-blue-500 hover:text-blue-700 dark:hover:text-blue-400"> | |
| <i class="fas fa-edit mr-1"></i> Edit | |
| </button> | |
| <button class="delete-btn px-3 py-1 text-sm text-red-500 hover:text-red-700 dark:hover:text-red-400"> | |
| <i class="fas fa-trash mr-1"></i> Delete | |
| </button> | |
| </div> | |
| `; | |
| return noteElement; | |
| } | |
| // Update empty state visibility | |
| function updateEmptyState() { | |
| if (notes.length === 0) { | |
| emptyState.classList.remove('hidden'); | |
| } else { | |
| emptyState.classList.add('hidden'); | |
| } | |
| } | |
| // Update clear all button visibility | |
| function updateClearAllButton() { | |
| if (notes.length > 0) { | |
| clearAllBtn.classList.remove('hidden'); | |
| } else { | |
| clearAllBtn.classList.add('hidden'); | |
| } | |
| } | |
| // Save notes to localStorage | |
| function saveNotes() { | |
| localStorage.setItem('notes', JSON.stringify(notes)); | |
| renderNotes(); | |
| updateEmptyState(); | |
| updateClearAllButton(); | |
| } | |
| // Open note editor | |
| function openNoteEditor(note = null) { | |
| if (note) { | |
| currentNoteId = note.id; | |
| editorTitle.textContent = 'Edit Note'; | |
| noteTitleInput.value = note.title; | |
| noteContentInput.value = note.content; | |
| notePinnedInput.checked = note.pinned; | |
| } else { | |
| currentNoteId = null; | |
| editorTitle.textContent = 'New Note'; | |
| noteTitleInput.value = ''; | |
| noteContentInput.value = ''; | |
| notePinnedInput.checked = false; | |
| } | |
| noteEditorModal.classList.remove('hidden'); | |
| noteTitleInput.focus(); | |
| } | |
| // Close note editor | |
| function closeNoteEditor() { | |
| noteEditorModal.classList.add('hidden'); | |
| currentNoteId = null; | |
| } | |
| // Save note | |
| function saveNote() { | |
| const title = noteTitleInput.value.trim(); | |
| const content = noteContentInput.value.trim(); | |
| const pinned = notePinnedInput.checked; | |
| if (!title && !content) { | |
| // Don't save empty notes | |
| closeNoteEditor(); | |
| return; | |
| } | |
| const now = new Date(); | |
| if (currentNoteId) { | |
| // Update existing note | |
| const noteIndex = notes.findIndex(n => n.id === currentNoteId); | |
| if (noteIndex !== -1) { | |
| notes[noteIndex] = { | |
| ...notes[noteIndex], | |
| title: title || 'Untitled Note', | |
| content, | |
| pinned, | |
| updatedAt: now.getTime() | |
| }; | |
| } | |
| } else { | |
| // Create new note | |
| const newNote = { | |
| id: Date.now().toString(), | |
| title: title || 'Untitled Note', | |
| content, | |
| pinned, | |
| createdAt: now.getTime(), | |
| updatedAt: now.getTime() | |
| }; | |
| notes.unshift(newNote); | |
| } | |
| saveNotes(); | |
| closeNoteEditor(); | |
| } | |
| // Delete note | |
| function deleteNote(id) { | |
| notes = notes.filter(note => note.id !== id); | |
| saveNotes(); | |
| } | |
| // Toggle note pinned status | |
| function togglePinned(id) { | |
| const noteIndex = notes.findIndex(n => n.id === id); | |
| if (noteIndex !== -1) { | |
| notes[noteIndex].pinned = !notes[noteIndex].pinned; | |
| notes[noteIndex].updatedAt = new Date().getTime(); | |
| saveNotes(); | |
| } | |
| } | |
| // Show confirmation dialog | |
| function showConfirmation(title, message, action) { | |
| confirmTitle.textContent = title; | |
| confirmMessage.textContent = message; | |
| actionToConfirm = action; | |
| confirmModal.classList.remove('hidden'); | |
| } | |
| // Clear all notes | |
| function clearAllNotes() { | |
| notes = []; | |
| saveNotes(); | |
| } | |
| // Toggle dark/light theme | |
| function toggleTheme() { | |
| const html = document.documentElement; | |
| if (html.classList.contains('dark')) { | |
| html.classList.remove('dark'); | |
| localStorage.setItem('theme', 'light'); | |
| } else { | |
| html.classList.add('dark'); | |
| localStorage.setItem('theme', 'dark'); | |
| } | |
| } | |
| // Event Listeners | |
| addNoteBtn.addEventListener('click', () => openNoteEditor()); | |
| closeEditorBtn.addEventListener('click', closeNoteEditor); | |
| cancelNoteBtn.addEventListener('click', closeNoteEditor); | |
| saveNoteBtn.addEventListener('click', saveNote); | |
| // Handle clicks on notes container (event delegation) | |
| notesContainer.addEventListener('click', (e) => { | |
| const noteElement = e.target.closest('.note-animation'); | |
| if (!noteElement) return; | |
| const noteId = noteElement.dataset.id; | |
| const note = notes.find(n => n.id === noteId); | |
| if (e.target.classList.contains('edit-btn') || e.target.closest('.edit-btn')) { | |
| openNoteEditor(note); | |
| } else if (e.target.classList.contains('delete-btn') || e.target.closest('.delete-btn')) { | |
| showConfirmation( | |
| 'Delete Note', | |
| 'Are you sure you want to delete this note? This action cannot be undone.', | |
| () => deleteNote(noteId) | |
| ); | |
| } else if (e.target.classList.contains('pin-btn') || e.target.closest('.pin-btn')) { | |
| togglePinned(noteId); | |
| } | |
| }); | |
| // Clear all notes confirmation | |
| clearAllBtn.addEventListener('click', () => { | |
| showConfirmation( | |
| 'Clear All Notes', | |
| 'Are you sure you want to delete all notes? This action cannot be undone.', | |
| clearAllNotes | |
| ); | |
| }); | |
| // Confirmation modal events | |
| cancelConfirmBtn.addEventListener('click', () => { | |
| confirmModal.classList.add('hidden'); | |
| actionToConfirm = null; | |
| }); | |
| confirmActionBtn.addEventListener | |
| </html> |