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> |