mirror of
https://github.com/thejakubruzicka/MNotes.git
synced 2025-07-10 14:34:05 +02:00
1460 lines
No EOL
48 KiB
HTML
1460 lines
No EOL
48 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>MNotes</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/material-components-web/14.0.0/material-components-web.min.css">
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,1,0">
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap">
|
|
<style>
|
|
:root {
|
|
/* Material 3 (You/Expressive) Colors - Light Theme */
|
|
--primary-color: #006A6A;
|
|
--on-primary-color: #FFFFFF;
|
|
--primary-container-color: #6FF7F7;
|
|
--on-primary-container-color: #002020;
|
|
--secondary-color: #4A6363;
|
|
--on-secondary-color: #FFFFFF;
|
|
--secondary-container-color: #CCE8E7;
|
|
--on-secondary-container-color: #051F1F;
|
|
--tertiary-color: #4B607C;
|
|
--on-tertiary-color: #FFFFFF;
|
|
--tertiary-container-color: #D3E4FF;
|
|
--on-tertiary-container-color: #041C35;
|
|
--error-color: #BA1A1A;
|
|
--on-error-color: #FFFFFF;
|
|
--error-container-color: #FFDAD6;
|
|
--on-error-container-color: #410002;
|
|
--surface-color: #FAFDFC;
|
|
--on-surface-color: #191C1C;
|
|
--surface-variant-color: #DAE5E3;
|
|
--on-surface-variant-color: #3F4948;
|
|
--outline-color: #6F7978;
|
|
--background-color: #FAFDFC;
|
|
--on-background-color: #191C1C;
|
|
--elevation-level-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);
|
|
--elevation-level-2: 0px 2px 6px 2px rgba(0, 0, 0, 0.15);
|
|
--elevation-level-3: 0px 4px 8px 3px rgba(0, 0, 0, 0.15);
|
|
--surface-tint-color: #006A6A;
|
|
}
|
|
|
|
<<<<<<< HEAD
|
|
<!-- Roboto Font (recommended for Material Design) -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
|
|
|
<!-- Material Symbols -->
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
|
|
|
<!-- External stylesheet -->
|
|
<link rel="stylesheet" href="style.css">
|
|
|
|
<!-- Import map for Material Web Components -->
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"@material/web/": "https://esm.run/@material/web/"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- Import Material Web Components -->
|
|
<script type="module">
|
|
import '@material/web/all.js';
|
|
import {styles as typescaleStyles} from '@material/web/typography/md-typescale-styles.js';
|
|
|
|
// Apply typescale styles to the document
|
|
document.adoptedStyleSheets.push(typescaleStyles.styleSheet);
|
|
</script>
|
|
|
|
<style>
|
|
/* Theme variables and overrides */
|
|
:root {
|
|
/* Light theme is the default in style.css */
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
/* Dark theme overrides */
|
|
body.dark-theme {
|
|
background-color: #141218 !important;
|
|
color: #E6E0E9 !important;
|
|
}
|
|
|
|
/* Force Material component theming */
|
|
body.dark-theme {
|
|
--md-sys-color-background: #141218;
|
|
--md-sys-color-on-background: #E6E0E9;
|
|
--md-sys-color-surface: #1C1B1F;
|
|
--md-sys-color-on-surface: #E6E0E9;
|
|
--md-sys-color-primary: #D0BCFF;
|
|
--md-sys-color-on-primary: #381E72;
|
|
--md-sys-color-secondary: #CCC2DC;
|
|
--md-sys-color-on-secondary: #332D41;
|
|
--md-sys-color-surface-container: #211F26;
|
|
--md-sys-color-surface-container-highest: #36343B;
|
|
}
|
|
|
|
body.light-theme {
|
|
background-color: #FEF7FF !important;
|
|
color: #1D1B20 !important;
|
|
--md-sys-color-background: #FEF7FF;
|
|
--md-sys-color-on-background: #1D1B20;
|
|
--md-sys-color-surface: #FDFCFB;
|
|
--md-sys-color-on-surface: #1D1B20;
|
|
--md-sys-color-primary: #6750A4;
|
|
--md-sys-color-on-primary: #FFFFFF;
|
|
--md-sys-color-secondary: #625B71;
|
|
--md-sys-color-on-secondary: #FFFFFF;
|
|
--md-sys-color-surface-container: #F3EDF7;
|
|
--md-sys-color-surface-container-highest: #E6E0E9;
|
|
}
|
|
|
|
/* Specific component overrides */
|
|
body.dark-theme .note-card {
|
|
background-color: #4A4458 !important;
|
|
color: #E8DEF8 !important;
|
|
}
|
|
|
|
body.dark-theme md-dialog {
|
|
--md-dialog-container-color: #2B2930;
|
|
--md-dialog-headline-color: #E6E0E9;
|
|
--md-dialog-supporting-text-color: #E6E0E9;
|
|
}
|
|
|
|
/* Base styles */
|
|
body {
|
|
font-family: 'Roboto', sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
opacity: 0; /* Start hidden to prevent FOUC */
|
|
transition: opacity 0.3s ease-in-out;
|
|
}
|
|
|
|
body.loaded {
|
|
opacity: 1; /* Show once components are defined */
|
|
}
|
|
|
|
/* Force components to use theme variables */
|
|
md-icon-button {
|
|
--md-icon-button-icon-color: var(--md-sys-color-primary);
|
|
}
|
|
|
|
#appTitle {
|
|
color: var(--md-sys-color-primary);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1 class="md-typescale-headline-large" id="appTitle">MNotes</h1>
|
|
<md-icon-button id="themeToggleBtn" aria-label="Toggle theme">
|
|
<md-icon>dark_mode</md-icon>
|
|
</md-icon-button>
|
|
</header>
|
|
|
|
<div id="notesList" class="md-typescale-body-medium">
|
|
<!-- Notes will be rendered here by JavaScript -->
|
|
</div>
|
|
|
|
<md-fab aria-label="Add Note" id="addNoteFab">
|
|
<md-icon slot="icon">add</md-icon>
|
|
</md-fab>
|
|
</div>
|
|
|
|
<!-- Note Dialog (for creating/editing) -->
|
|
<md-dialog id="noteDialog">
|
|
<div slot="headline">Add/Edit Note</div>
|
|
<div slot="content">
|
|
<form id="noteForm">
|
|
<input type="hidden" id="noteIdInput">
|
|
<md-filled-text-field label="Title" id="noteTitleInput" required style="width: 100%; margin-bottom: 16px;"></md-filled-text-field>
|
|
<md-outlined-text-field label="Content" id="noteContentInput" type="textarea" rows="5" required style="width: 100%; margin-bottom: 16px;"></md-outlined-text-field>
|
|
</form>
|
|
</div>
|
|
<div slot="actions">
|
|
<md-text-button id="cancelNoteBtn">Cancel</md-text-button>
|
|
<md-filled-button id="saveNoteBtn">Save</md-filled-button>
|
|
</div>
|
|
</md-dialog>
|
|
|
|
<!-- Confirmation Dialog -->
|
|
<md-dialog id="confirmDeleteDialog">
|
|
<div slot="headline">Confirm Delete</div>
|
|
<div slot="content" class="md-typescale-body-medium">
|
|
Are you sure you want to delete this note?
|
|
</div>
|
|
<div slot="actions">
|
|
<md-text-button id="cancelDeleteBtn">Cancel</md-text-button>
|
|
<md-filled-button id="confirmDeleteBtn">Delete</md-filled-button>
|
|
</div>
|
|
</md-dialog>
|
|
|
|
<script>
|
|
// Wait for both DOM content and Web Components to be ready
|
|
function initApp() {
|
|
// Check if critical components are defined
|
|
if (!customElements.get('md-dialog') || !customElements.get('md-fab')) {
|
|
// If components aren't ready yet, try again in a short while
|
|
setTimeout(initApp, 100);
|
|
return;
|
|
}
|
|
|
|
console.log('Initializing MNotes app...');
|
|
|
|
// DOM elements
|
|
const notesList = document.getElementById('notesList');
|
|
const addNoteFab = document.getElementById('addNoteFab');
|
|
const noteDialog = document.getElementById('noteDialog');
|
|
const confirmDeleteDialog = document.getElementById('confirmDeleteDialog');
|
|
const noteForm = document.getElementById('noteForm');
|
|
const noteIdInput = document.getElementById('noteIdInput');
|
|
const noteTitleInput = document.getElementById('noteTitleInput');
|
|
const noteContentInput = document.getElementById('noteContentInput');
|
|
const themeToggleBtn = document.getElementById('themeToggleBtn');
|
|
const body = document.body;
|
|
|
|
let notes = [];
|
|
let currentEditingId = null;
|
|
let noteToDeleteId = null;
|
|
|
|
// --- Theming ---
|
|
function applyTheme(theme) { // theme is 'light' or 'dark'
|
|
console.log('Applying theme:', theme);
|
|
|
|
// First, remove both themes to ensure clean state
|
|
body.classList.remove('light-theme', 'dark-theme');
|
|
|
|
// Force the body to reset styles
|
|
body.style.transition = 'none';
|
|
|
|
// Apply new theme with a small delay for better CSS processing
|
|
setTimeout(() => {
|
|
// Add the theme class
|
|
body.classList.add(theme + '-theme');
|
|
|
|
// Force a repaint with inline styles
|
|
body.style.backgroundColor = theme === 'dark' ? '#141218' : '#FEF7FF';
|
|
body.style.color = theme === 'dark' ? '#E6E0E9' : '#1D1B20';
|
|
|
|
// Re-enable transitions
|
|
setTimeout(() => {
|
|
body.style.transition = 'all 0.3s ease-in-out';
|
|
}, 50);
|
|
|
|
// Save preference
|
|
localStorage.setItem('mnotes-theme', theme);
|
|
|
|
// Update theme toggle icon
|
|
const icon = themeToggleBtn.querySelector('md-icon');
|
|
if (icon) {
|
|
if (theme === 'dark') {
|
|
icon.textContent = 'light_mode';
|
|
themeToggleBtn.ariaLabel = "Activate light theme";
|
|
} else {
|
|
icon.textContent = 'dark_mode';
|
|
themeToggleBtn.ariaLabel = "Activate dark theme";
|
|
}
|
|
}
|
|
}, 10);
|
|
}
|
|
|
|
function toggleTheme() {
|
|
console.log('Toggle theme clicked');
|
|
const currentTheme = localStorage.getItem('mnotes-theme') ||
|
|
(body.classList.contains('dark-theme') ? 'dark' : 'light');
|
|
applyTheme(currentTheme === 'light' ? 'dark' : 'light');
|
|
}
|
|
|
|
function loadInitialTheme() {
|
|
const savedTheme = localStorage.getItem('mnotes-theme');
|
|
if (savedTheme) {
|
|
applyTheme(savedTheme);
|
|
} else {
|
|
// Prefer system theme if no preference saved
|
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
applyTheme('dark');
|
|
} else {
|
|
applyTheme('light'); // Default to light
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure themeToggleBtn exists before attaching event
|
|
if (themeToggleBtn) {
|
|
themeToggleBtn.addEventListener('click', toggleTheme);
|
|
console.log('Theme toggle button event listener attached');
|
|
} else {
|
|
console.error('Theme toggle button not found');
|
|
}
|
|
|
|
// --- Data Persistence ---
|
|
function loadNotes() {
|
|
try {
|
|
const storedNotes = localStorage.getItem('material-notes');
|
|
console.log('Loading notes from localStorage:', storedNotes);
|
|
notes = storedNotes ? JSON.parse(storedNotes) : [];
|
|
renderNotes();
|
|
} catch (error) {
|
|
console.error('Error loading notes:', error);
|
|
notes = [];
|
|
renderNotes();
|
|
}
|
|
}
|
|
|
|
function saveNotes() {
|
|
try {
|
|
console.log('Saving notes to localStorage:', notes);
|
|
localStorage.setItem('material-notes', JSON.stringify(notes));
|
|
} catch (error) {
|
|
console.error('Error saving notes:', error);
|
|
alert('Failed to save notes. If you\'re in private browsing mode, try regular browsing mode.');
|
|
}
|
|
}
|
|
|
|
// --- Rendering Notes ---
|
|
function renderNotes() {
|
|
if (!notesList) {
|
|
console.error('Notes list element not found');
|
|
return;
|
|
}
|
|
|
|
console.log('Rendering notes:', notes);
|
|
notesList.innerHTML = ''; // Clear existing notes
|
|
|
|
if (notes.length === 0) {
|
|
const p = document.createElement('p');
|
|
p.textContent = "No notes yet. Click the + button to add one!";
|
|
notesList.appendChild(p);
|
|
return;
|
|
}
|
|
|
|
notes.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); // Sort by most recently modified
|
|
|
|
notes.forEach(note => {
|
|
const noteCard = document.createElement('div');
|
|
noteCard.className = 'note-card';
|
|
|
|
const elevation = document.createElement('md-elevation'); // Add MWC elevation
|
|
elevation.setAttribute('level', '1');
|
|
noteCard.appendChild(elevation);
|
|
|
|
const title = document.createElement('h3');
|
|
title.textContent = note.title;
|
|
title.className = 'md-typescale-headline-small';
|
|
|
|
const content = document.createElement('p');
|
|
content.textContent = note.content.substring(0, 150) + (note.content.length > 150 ? '...' : '');
|
|
content.className = 'md-typescale-body-medium';
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'note-card-actions';
|
|
|
|
const editButton = document.createElement('md-icon-button');
|
|
editButton.innerHTML = `<md-icon>edit</md-icon>`;
|
|
editButton.ariaLabel = `Edit note: ${note.title}`;
|
|
editButton.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
openNoteDialog(note);
|
|
});
|
|
|
|
const deleteButton = document.createElement('md-icon-button');
|
|
deleteButton.innerHTML = `<md-icon>delete</md-icon>`;
|
|
deleteButton.ariaLabel = `Delete note: ${note.title}`;
|
|
deleteButton.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
openDeleteDialog(note.id);
|
|
});
|
|
|
|
actions.appendChild(editButton);
|
|
actions.appendChild(deleteButton);
|
|
|
|
noteCard.appendChild(title);
|
|
noteCard.appendChild(content);
|
|
noteCard.appendChild(actions);
|
|
notesList.appendChild(noteCard);
|
|
});
|
|
}
|
|
|
|
// --- Note Operations ---
|
|
function openNoteDialog(note = null) {
|
|
if (!noteDialog || !noteForm || !noteTitleInput || !noteContentInput) {
|
|
console.error('Note dialog elements not found');
|
|
return;
|
|
}
|
|
|
|
console.log('Opening note dialog', note ? 'for editing' : 'for new note');
|
|
noteForm.reset(); // Clear form fields
|
|
// Reset MWC text field states (e.g. error messages)
|
|
noteTitleInput.errorText = "";
|
|
noteTitleInput.error = false;
|
|
noteContentInput.errorText = "";
|
|
noteContentInput.error = false;
|
|
|
|
if (note) {
|
|
currentEditingId = note.id;
|
|
noteTitleInput.value = note.title;
|
|
noteContentInput.value = note.content; // Full content for editing
|
|
|
|
const headline = noteDialog.querySelector('[slot="headline"]');
|
|
if (headline) headline.textContent = 'Edit Note';
|
|
} else {
|
|
currentEditingId = null;
|
|
const headline = noteDialog.querySelector('[slot="headline"]');
|
|
if (headline) headline.textContent = 'Add New Note';
|
|
}
|
|
|
|
// Open the dialog
|
|
noteDialog.open = true;
|
|
}
|
|
|
|
// Direct save function - bypasses the dialog event issues
|
|
function saveNote() {
|
|
const title = noteTitleInput.value.trim();
|
|
const content = noteContentInput.value.trim();
|
|
console.log('Saving note:', { title, content });
|
|
|
|
let isValid = true;
|
|
if (!title) {
|
|
noteTitleInput.error = true;
|
|
noteTitleInput.errorText = "Title cannot be empty.";
|
|
isValid = false;
|
|
} else {
|
|
noteTitleInput.error = false;
|
|
noteTitleInput.errorText = "";
|
|
}
|
|
if (!content) {
|
|
noteContentInput.error = true;
|
|
noteContentInput.errorText = "Content cannot be empty.";
|
|
isValid = false;
|
|
} else {
|
|
noteContentInput.error = false;
|
|
noteContentInput.errorText = "";
|
|
}
|
|
|
|
if (!isValid) {
|
|
console.log('Validation failed');
|
|
return false;
|
|
}
|
|
|
|
const now = new Date().toISOString();
|
|
if (currentEditingId) {
|
|
const noteIndex = notes.findIndex(n => n.id === currentEditingId);
|
|
if (noteIndex > -1) {
|
|
notes[noteIndex].title = title;
|
|
notes[noteIndex].content = content;
|
|
notes[noteIndex].lastModified = now;
|
|
console.log('Updated existing note at index', noteIndex);
|
|
}
|
|
} else {
|
|
const newNote = {
|
|
id: Date.now().toString(),
|
|
title,
|
|
content,
|
|
createdAt: now,
|
|
lastModified: now
|
|
};
|
|
notes.push(newNote);
|
|
console.log('Added new note:', newNote);
|
|
}
|
|
|
|
// Close dialog, save notes and update UI
|
|
noteDialog.open = false;
|
|
saveNotes();
|
|
renderNotes();
|
|
|
|
// Reset for next time
|
|
currentEditingId = null;
|
|
noteForm.reset();
|
|
noteTitleInput.error = false;
|
|
noteTitleInput.errorText = "";
|
|
noteContentInput.error = false;
|
|
noteContentInput.errorText = "";
|
|
|
|
return true;
|
|
}
|
|
|
|
// Handle dialog close event (just for logging/cleanup)
|
|
function handleDialogClose(event) {
|
|
console.log('Note dialog closed with action:', event?.detail?.action);
|
|
|
|
// Reset state regardless of how dialog was closed
|
|
currentEditingId = null;
|
|
if (noteForm) noteForm.reset();
|
|
if (noteTitleInput) {
|
|
noteTitleInput.error = false;
|
|
noteTitleInput.errorText = "";
|
|
}
|
|
if (noteContentInput) {
|
|
noteContentInput.error = false;
|
|
noteContentInput.errorText = "";
|
|
}
|
|
}
|
|
|
|
function openDeleteDialog(id) {
|
|
if (!confirmDeleteDialog) {
|
|
console.error('Confirm delete dialog not found');
|
|
return;
|
|
}
|
|
|
|
console.log('Opening delete confirmation dialog for note ID:', id);
|
|
noteToDeleteId = id;
|
|
confirmDeleteDialog.open = true;
|
|
}
|
|
|
|
function confirmDelete() {
|
|
if (noteToDeleteId) {
|
|
console.log('Deleting note with ID:', noteToDeleteId);
|
|
notes = notes.filter(note => note.id !== noteToDeleteId);
|
|
saveNotes();
|
|
renderNotes();
|
|
}
|
|
|
|
// Close dialog and reset state
|
|
confirmDeleteDialog.open = false;
|
|
noteToDeleteId = null;
|
|
}
|
|
|
|
function handleDeleteDialogClose() {
|
|
// Just reset state when dialog is closed
|
|
console.log('Delete dialog closed');
|
|
noteToDeleteId = null;
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
if (addNoteFab) {
|
|
addNoteFab.addEventListener('click', () => openNoteDialog());
|
|
console.log('Add note FAB event listener attached');
|
|
} else {
|
|
console.error('Add note FAB not found');
|
|
}
|
|
|
|
// Dialog event listeners
|
|
if (noteDialog) {
|
|
noteDialog.addEventListener('closed', handleDialogClose);
|
|
console.log('Note dialog event listener attached');
|
|
} else {
|
|
console.error('Note dialog not found');
|
|
}
|
|
|
|
// Button event listeners
|
|
const saveNoteBtn = document.getElementById('saveNoteBtn');
|
|
if (saveNoteBtn) {
|
|
saveNoteBtn.addEventListener('click', saveNote);
|
|
console.log('Save note button event listener attached');
|
|
} else {
|
|
console.error('Save note button not found');
|
|
}
|
|
|
|
const cancelNoteBtn = document.getElementById('cancelNoteBtn');
|
|
if (cancelNoteBtn) {
|
|
cancelNoteBtn.addEventListener('click', () => {
|
|
noteDialog.open = false;
|
|
});
|
|
console.log('Cancel note button event listener attached');
|
|
} else {
|
|
console.error('Cancel note button not found');
|
|
}
|
|
// Delete dialog button listeners
|
|
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
|
if (confirmDeleteBtn) {
|
|
confirmDeleteBtn.addEventListener('click', confirmDelete);
|
|
console.log('Confirm delete button event listener attached');
|
|
} else {
|
|
console.error('Confirm delete button not found');
|
|
}
|
|
|
|
const cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
|
|
if (cancelDeleteBtn) {
|
|
cancelDeleteBtn.addEventListener('click', () => {
|
|
confirmDeleteDialog.open = false;
|
|
noteToDeleteId = null;
|
|
});
|
|
console.log('Cancel delete button event listener attached');
|
|
} else {
|
|
console.error('Cancel delete button not found');
|
|
}
|
|
|
|
if (confirmDeleteDialog) {
|
|
confirmDeleteDialog.addEventListener('closed', handleDeleteDialogClose);
|
|
console.log('Confirm delete dialog event listener attached');
|
|
} else {
|
|
console.error('Confirm delete dialog not found');
|
|
}
|
|
|
|
// --- Initial Load ---
|
|
loadInitialTheme();
|
|
loadNotes();
|
|
|
|
// Show body once components are fully initialized
|
|
document.body.classList.add('loaded');
|
|
console.log('MNotes app initialized successfully');
|
|
}
|
|
|
|
// Start initialization when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
console.log('DOM content loaded, initializing app...');
|
|
|
|
// Set a base theme class before components load
|
|
document.body.classList.add('light-theme');
|
|
|
|
// Give a longer delay for Web Components to register
|
|
setTimeout(initApp, 200);
|
|
});
|
|
</script>
|
|
=======
|
|
.theme-dark {
|
|
/* Material 3 (You/Expressive) Colors - Dark Theme */
|
|
--primary-color: #4CDADA;
|
|
--on-primary-color: #003737;
|
|
--primary-container-color: #004F4F;
|
|
--on-primary-container-color: #6FF7F7;
|
|
--secondary-color: #B0CCCC;
|
|
--on-secondary-color: #1B3534;
|
|
--secondary-container-color: #324B4B;
|
|
--on-secondary-container-color: #CCE8E7;
|
|
--tertiary-color: #B4C8E9;
|
|
--on-tertiary-color: #1C314B;
|
|
--tertiary-container-color: #334863;
|
|
--on-tertiary-container-color: #D3E4FF;
|
|
--error-color: #FFB4AB;
|
|
--on-error-color: #690005;
|
|
--error-container-color: #93000A;
|
|
--on-error-container-color: #FFDAD6;
|
|
--surface-color: #191C1C;
|
|
--on-surface-color: #E0E3E2;
|
|
--surface-variant-color: #3F4948;
|
|
--on-surface-variant-color: #BEC9C7;
|
|
--outline-color: #899392;
|
|
--background-color: #191C1C;
|
|
--on-background-color: #E0E3E2;
|
|
--elevation-level-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.35);
|
|
--elevation-level-2: 0px 2px 6px 2px rgba(0, 0, 0, 0.35);
|
|
--elevation-level-3: 0px 4px 8px 3px rgba(0, 0, 0, 0.35);
|
|
--surface-tint-color: #4CDADA;
|
|
}
|
|
|
|
.theme-sepia {
|
|
/* Material 3 (You/Expressive) Colors - Sepia Theme */
|
|
--primary-color: #8B5000;
|
|
--on-primary-color: #FFFFFF;
|
|
--primary-container-color: #FFDDB7;
|
|
--on-primary-container-color: #2C1700;
|
|
--secondary-color: #735A41;
|
|
--on-secondary-color: #FFFFFF;
|
|
--secondary-container-color: #FFDDBD;
|
|
--on-secondary-container-color: #291805;
|
|
--tertiary-color: #5B6237;
|
|
--on-tertiary-color: #FFFFFF;
|
|
--tertiary-container-color: #DFE8B2;
|
|
--on-tertiary-container-color: #181E00;
|
|
--error-color: #BA1A1A;
|
|
--on-error-color: #FFFFFF;
|
|
--error-container-color: #FFDAD6;
|
|
--on-error-container-color: #410002;
|
|
--surface-color: #FFFBFF;
|
|
--on-surface-color: #201B16;
|
|
--surface-variant-color: #F0E0CF;
|
|
--on-surface-variant-color: #4E4539;
|
|
--outline-color: #7F7667;
|
|
--background-color: #FFF8F3;
|
|
--on-background-color: #201B16;
|
|
--elevation-level-1: 0px 1px 3px 1px rgba(139, 80, 0, 0.15);
|
|
--elevation-level-2: 0px 2px 6px 2px rgba(139, 80, 0, 0.15);
|
|
--elevation-level-3: 0px 4px 8px 3px rgba(139, 80, 0, 0.15);
|
|
--surface-tint-color: #8B5000;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Google Sans', 'Roboto', sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
background-color: var(--background-color);
|
|
color: var(--on-background-color);
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header {
|
|
background-color: var(--primary-color);
|
|
color: var(--on-primary-color);
|
|
padding: 16px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
box-shadow: var(--elevation-level-2);
|
|
z-index: 10;
|
|
}
|
|
|
|
.app-title {
|
|
font-size: 1.5rem;
|
|
font-weight: 500;
|
|
margin: 0;
|
|
font-family: 'Google Sans', sans-serif;
|
|
}
|
|
|
|
.main-container {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sidebar {
|
|
width: 280px;
|
|
background-color: var(--surface-color);
|
|
border-right: 1px solid var(--outline-color);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
box-shadow: var(--elevation-level-1);
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 16px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 1px solid var(--outline-color);
|
|
background-color: var(--surface-variant-color);
|
|
color: var(--on-surface-variant-color);
|
|
}
|
|
|
|
.note-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
flex-grow: 1;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.note-item {
|
|
padding: 16px;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid var(--outline-color);
|
|
transition: background-color 0.2s, transform 0.1s;
|
|
border-radius: 12px;
|
|
margin: 8px;
|
|
}
|
|
|
|
.note-item:hover {
|
|
background-color: var(--surface-variant-color);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.note-item.active {
|
|
background-color: var(--primary-container-color);
|
|
color: var(--on-primary-container-color);
|
|
box-shadow: var(--elevation-level-1);
|
|
}
|
|
|
|
.note-item-title {
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.note-item-preview {
|
|
font-size: 0.85rem;
|
|
opacity: 0.75;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.editor-container {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.toolbar {
|
|
background-color: var(--surface-color);
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
|
padding: 8px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.editor {
|
|
flex: 1;
|
|
padding: 16px;
|
|
overflow-y: auto;
|
|
outline: none;
|
|
font-size: 1rem;
|
|
line-height: 1.6;
|
|
border: none;
|
|
background-color: var(--surface-color);
|
|
color: var(--on-surface-color);
|
|
resize: none;
|
|
}
|
|
|
|
.button {
|
|
background-color: var(--primary-container-color);
|
|
color: var(--on-primary-container-color);
|
|
border: none;
|
|
border-radius: 20px;
|
|
padding: 10px 16px;
|
|
font-family: 'Google Sans', sans-serif;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: all 0.2s ease;
|
|
box-shadow: var(--elevation-level-1);
|
|
}
|
|
|
|
.button:hover {
|
|
background-color: var(--primary-container-color);
|
|
opacity: 0.9;
|
|
box-shadow: var(--elevation-level-2);
|
|
}
|
|
|
|
.button-icon {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.theme-selector {
|
|
margin-left: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 8px;
|
|
background-color: var(--surface-variant-color);
|
|
border-radius: 24px;
|
|
}
|
|
|
|
.theme-button {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
border: 2px solid transparent;
|
|
cursor: pointer;
|
|
transition: transform 0.2s, border-color 0.2s;
|
|
}
|
|
|
|
.theme-button.active {
|
|
border-color: var(--primary-color);
|
|
transform: scale(1.2);
|
|
}
|
|
|
|
.theme-light {
|
|
background-color: #FFFBFE;
|
|
}
|
|
|
|
.theme-dark {
|
|
background-color: #1C1B1F;
|
|
}
|
|
|
|
.theme-sepia {
|
|
background-color: #F5F5DC;
|
|
}
|
|
|
|
.status-bar {
|
|
padding: 8px 16px;
|
|
font-size: 0.75rem;
|
|
background-color: var(--surface-color);
|
|
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-content {
|
|
background-color: var(--surface-color);
|
|
padding: 32px;
|
|
border-radius: 28px;
|
|
width: 100%;
|
|
max-width: 400px;
|
|
box-shadow: var(--elevation-level-3);
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 1.5rem;
|
|
font-weight: 500;
|
|
margin-top: 0;
|
|
margin-bottom: 24px;
|
|
color: var(--on-surface-color);
|
|
font-family: 'Google Sans', sans-serif;
|
|
}
|
|
|
|
.form-field {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.input-field {
|
|
width: 100%;
|
|
padding: 16px;
|
|
font-family: 'Google Sans', 'Roboto', sans-serif;
|
|
font-size: 1rem;
|
|
border: 2px solid var(--outline-color);
|
|
border-radius: 16px;
|
|
background-color: var(--surface-color);
|
|
color: var(--on-surface-color);
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.input-field:focus {
|
|
border-color: var(--primary-color);
|
|
outline: none;
|
|
}
|
|
|
|
.modal-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
}
|
|
|
|
.info-message {
|
|
background-color: var(--container-color);
|
|
color: var(--on-container-color);
|
|
padding: 8px 16px;
|
|
margin: 8px 0;
|
|
border-radius: 4px;
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.sidebar {
|
|
position: absolute;
|
|
left: -250px;
|
|
height: 100%;
|
|
transition: left 0.3s ease;
|
|
z-index: 100;
|
|
}
|
|
|
|
.sidebar.open {
|
|
left: 0;
|
|
}
|
|
|
|
.toggle-sidebar {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.material-symbols-outlined {
|
|
font-size: 24px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<div class="left-section">
|
|
<button id="toggleSidebar" class="button">
|
|
<span class="material-symbols-rounded">menu</span>
|
|
</button>
|
|
<h1 class="app-title">MNotes</h1>
|
|
</div>
|
|
<div class="theme-selector">
|
|
<button class="theme-button theme-light active" data-theme="light" title="Light theme"></button>
|
|
<button class="theme-button theme-dark" data-theme="dark" title="Dark theme"></button>
|
|
<button class="theme-button theme-sepia" data-theme="sepia" title="Sepia theme"></button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-container">
|
|
<div class="sidebar" id="sidebar">
|
|
<div class="sidebar-header">
|
|
<h2>Notes</h2>
|
|
<button id="newNoteBtn" class="button">
|
|
<span class="material-symbols-rounded">add</span>
|
|
</button>
|
|
</div>
|
|
<ul class="note-list" id="noteList">
|
|
<!-- Note items will be added here -->
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="editor-container">
|
|
<div class="toolbar">
|
|
<button id="saveNote" class="button">
|
|
<span class="material-symbols-rounded">save</span>
|
|
Save
|
|
</button>
|
|
<button id="downloadNote" class="button">
|
|
<span class="material-symbols-rounded">download</span>
|
|
Download
|
|
</button>
|
|
<button id="githubSave" class="button">
|
|
<span class="material-symbols-rounded">cloud_upload</span>
|
|
Save to GitHub
|
|
</button>
|
|
<button id="deleteNote" class="button">
|
|
<span class="material-symbols-rounded">delete</span>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
<textarea id="editor" class="editor" placeholder="Start typing your note here..."></textarea>
|
|
<div class="status-bar">
|
|
<span id="wordCount">0 words</span>
|
|
<span id="lastSaved">Not saved yet</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- GitHub Auth Modal -->
|
|
<div class="modal" id="githubModal">
|
|
<div class="modal-content">
|
|
<h2 class="modal-title">GitHub Authentication</h2>
|
|
<div class="form-field">
|
|
<input type="text" id="githubUsername" class="input-field" placeholder="GitHub Username">
|
|
</div>
|
|
<div class="form-field">
|
|
<input type="password" id="githubToken" class="input-field" placeholder="Personal Access Token">
|
|
</div>
|
|
<div class="form-field">
|
|
<input type="text" id="githubRepo" class="input-field" placeholder="Repository Name">
|
|
</div>
|
|
<div class="info-message" id="githubMessage"></div>
|
|
<div class="modal-actions">
|
|
<button id="cancelGithubAuth" class="button">Cancel</button>
|
|
<button id="confirmGithubAuth" class="button">Connect</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New Note Modal -->
|
|
<div class="modal" id="newNoteModal">
|
|
<div class="modal-content">
|
|
<h2 class="modal-title">Create New Note</h2>
|
|
<div class="form-field">
|
|
<input type="text" id="newNoteTitle" class="input-field" placeholder="Note Title">
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button id="cancelNewNote" class="button">Cancel</button>
|
|
<button id="confirmNewNote" class="button">Create</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Main app logic
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// State variables
|
|
let notes = [];
|
|
let currentNoteId = null;
|
|
let githubAuth = {
|
|
username: '',
|
|
token: '',
|
|
repo: ''
|
|
};
|
|
|
|
// DOM elements
|
|
const editor = document.getElementById('editor');
|
|
const noteList = document.getElementById('noteList');
|
|
const wordCount = document.getElementById('wordCount');
|
|
const lastSaved = document.getElementById('lastSaved');
|
|
const toggleSidebarBtn = document.getElementById('toggleSidebar');
|
|
const sidebar = document.getElementById('sidebar');
|
|
const newNoteBtn = document.getElementById('newNoteBtn');
|
|
const saveNoteBtn = document.getElementById('saveNote');
|
|
const downloadNoteBtn = document.getElementById('downloadNote');
|
|
const githubSaveBtn = document.getElementById('githubSave');
|
|
const deleteNoteBtn = document.getElementById('deleteNote');
|
|
const themeButtons = document.querySelectorAll('.theme-button');
|
|
|
|
// Modals
|
|
const githubModal = document.getElementById('githubModal');
|
|
const githubUsername = document.getElementById('githubUsername');
|
|
const githubToken = document.getElementById('githubToken');
|
|
const githubRepo = document.getElementById('githubRepo');
|
|
const githubMessage = document.getElementById('githubMessage');
|
|
const confirmGithubAuth = document.getElementById('confirmGithubAuth');
|
|
const cancelGithubAuth = document.getElementById('cancelGithubAuth');
|
|
|
|
const newNoteModal = document.getElementById('newNoteModal');
|
|
const newNoteTitle = document.getElementById('newNoteTitle');
|
|
const confirmNewNote = document.getElementById('confirmNewNote');
|
|
const cancelNewNote = document.getElementById('cancelNewNote');
|
|
|
|
// Load data from localStorage
|
|
function loadFromLocalStorage() {
|
|
// Load notes
|
|
const savedNotes = localStorage.getItem('notes');
|
|
if (savedNotes) {
|
|
notes = JSON.parse(savedNotes);
|
|
renderNoteList();
|
|
}
|
|
|
|
// Load theme
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme) {
|
|
setTheme(savedTheme);
|
|
}
|
|
|
|
// Load GitHub auth
|
|
const savedGithubAuth = localStorage.getItem('githubAuth');
|
|
if (savedGithubAuth) {
|
|
githubAuth = JSON.parse(savedGithubAuth);
|
|
}
|
|
}
|
|
|
|
// Create a new note
|
|
function createNewNote(title) {
|
|
const newNote = {
|
|
id: Date.now().toString(),
|
|
title: title || 'Untitled Note',
|
|
content: '',
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString()
|
|
};
|
|
|
|
notes.push(newNote);
|
|
saveNotesToLocalStorage();
|
|
renderNoteList();
|
|
openNote(newNote.id);
|
|
}
|
|
|
|
// Open a note
|
|
function openNote(noteId) {
|
|
const note = notes.find(n => n.id === noteId);
|
|
if (!note) return;
|
|
|
|
currentNoteId = noteId;
|
|
editor.value = note.content;
|
|
updateWordCount();
|
|
updateLastSaved(note.updatedAt);
|
|
|
|
// Update active state in note list
|
|
const noteItems = document.querySelectorAll('.note-item');
|
|
noteItems.forEach(item => {
|
|
if (item.dataset.id === noteId) {
|
|
item.classList.add('active');
|
|
} else {
|
|
item.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Save the current note
|
|
function saveCurrentNote() {
|
|
if (!currentNoteId) return;
|
|
|
|
const noteIndex = notes.findIndex(n => n.id === currentNoteId);
|
|
if (noteIndex === -1) return;
|
|
|
|
notes[noteIndex].content = editor.value;
|
|
notes[noteIndex].updatedAt = new Date().toISOString();
|
|
|
|
saveNotesToLocalStorage();
|
|
updateLastSaved(notes[noteIndex].updatedAt);
|
|
renderNoteList();
|
|
}
|
|
|
|
// Delete the current note
|
|
function deleteCurrentNote() {
|
|
if (!currentNoteId) return;
|
|
|
|
const confirmDelete = confirm('Are you sure you want to delete this note?');
|
|
if (!confirmDelete) return;
|
|
|
|
notes = notes.filter(n => n.id !== currentNoteId);
|
|
saveNotesToLocalStorage();
|
|
renderNoteList();
|
|
|
|
if (notes.length > 0) {
|
|
openNote(notes[0].id);
|
|
} else {
|
|
currentNoteId = null;
|
|
editor.value = '';
|
|
updateWordCount();
|
|
updateLastSaved(null);
|
|
}
|
|
}
|
|
|
|
// Download the current note as Markdown
|
|
function downloadNoteAsMarkdown() {
|
|
if (!currentNoteId) return;
|
|
|
|
const note = notes.find(n => n.id === currentNoteId);
|
|
if (!note) return;
|
|
|
|
const filename = `${note.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.md`;
|
|
const content = note.content;
|
|
|
|
const blob = new Blob([content], { type: 'text/markdown' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Save to GitHub
|
|
async function saveToGithub() {
|
|
if (!currentNoteId || !githubAuth.username || !githubAuth.token || !githubAuth.repo) {
|
|
showGithubModal();
|
|
return;
|
|
}
|
|
|
|
const note = notes.find(n => n.id === currentNoteId);
|
|
if (!note) return;
|
|
|
|
const filename = `notes/${note.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.md`;
|
|
const content = note.content;
|
|
|
|
try {
|
|
// First, try to get the file to see if it exists
|
|
let sha;
|
|
try {
|
|
const getResponse = await fetch(`https://api.github.com/repos/${githubAuth.username}/${githubAuth.repo}/contents/${filename}`, {
|
|
headers: {
|
|
'Authorization': `token ${githubAuth.token}`
|
|
}
|
|
});
|
|
|
|
if (getResponse.status === 200) {
|
|
const fileData = await getResponse.json();
|
|
sha = fileData.sha;
|
|
}
|
|
} catch (error) {
|
|
// File doesn't exist, that's fine
|
|
}
|
|
|
|
// Now create or update the file
|
|
const response = await fetch(`https://api.github.com/repos/${githubAuth.username}/${githubAuth.repo}/contents/${filename}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `token ${githubAuth.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
message: `Update note: ${note.title}`,
|
|
content: btoa(unescape(encodeURIComponent(content))),
|
|
sha: sha
|
|
})
|
|
});
|
|
|
|
if (response.status === 200 || response.status === 201) {
|
|
alert('Note saved to GitHub successfully!');
|
|
} else {
|
|
const error = await response.json();
|
|
throw new Error(error.message);
|
|
}
|
|
} catch (error) {
|
|
alert(`Error saving to GitHub: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Show GitHub modal
|
|
function showGithubModal() {
|
|
githubUsername.value = githubAuth.username || '';
|
|
githubToken.value = githubAuth.token || '';
|
|
githubRepo.value = githubAuth.repo || '';
|
|
githubModal.style.display = 'flex';
|
|
}
|
|
|
|
// Hide GitHub modal
|
|
function hideGithubModal() {
|
|
githubModal.style.display = 'none';
|
|
}
|
|
|
|
// Show new note modal
|
|
function showNewNoteModal() {
|
|
newNoteTitle.value = '';
|
|
newNoteModal.style.display = 'flex';
|
|
newNoteTitle.focus();
|
|
}
|
|
|
|
// Hide new note modal
|
|
function hideNewNoteModal() {
|
|
newNoteModal.style.display = 'none';
|
|
}
|
|
|
|
// Save GitHub auth
|
|
function saveGithubAuth() {
|
|
githubAuth.username = githubUsername.value.trim();
|
|
githubAuth.token = githubToken.value.trim();
|
|
githubAuth.repo = githubRepo.value.trim();
|
|
|
|
if (!githubAuth.username || !githubAuth.token || !githubAuth.repo) {
|
|
githubMessage.textContent = 'All fields are required';
|
|
githubMessage.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
localStorage.setItem('githubAuth', JSON.stringify(githubAuth));
|
|
hideGithubModal();
|
|
|
|
// Try to save the current note to GitHub
|
|
if (currentNoteId) {
|
|
saveToGithub();
|
|
}
|
|
}
|
|
|
|
// Save notes to localStorage
|
|
function saveNotesToLocalStorage() {
|
|
localStorage.setItem('notes', JSON.stringify(notes));
|
|
}
|
|
|
|
// Render the note list
|
|
function renderNoteList() {
|
|
noteList.innerHTML = '';
|
|
|
|
if (notes.length === 0) {
|
|
const emptyItem = document.createElement('li');
|
|
emptyItem.className = 'note-item';
|
|
emptyItem.textContent = 'No notes yet';
|
|
noteList.appendChild(emptyItem);
|
|
return;
|
|
}
|
|
|
|
// Sort notes by updated date (newest first)
|
|
const sortedNotes = [...notes].sort((a, b) => {
|
|
return new Date(b.updatedAt) - new Date(a.updatedAt);
|
|
});
|
|
|
|
sortedNotes.forEach(note => {
|
|
const noteItem = document.createElement('li');
|
|
noteItem.className = 'note-item';
|
|
if (note.id === currentNoteId) {
|
|
noteItem.classList.add('active');
|
|
}
|
|
noteItem.dataset.id = note.id;
|
|
|
|
const titleElement = document.createElement('div');
|
|
titleElement.className = 'note-item-title';
|
|
titleElement.textContent = note.title;
|
|
|
|
const previewElement = document.createElement('div');
|
|
previewElement.className = 'note-item-preview';
|
|
previewElement.textContent = note.content.substring(0, 50) || 'Empty note';
|
|
|
|
noteItem.appendChild(titleElement);
|
|
noteItem.appendChild(previewElement);
|
|
|
|
noteItem.addEventListener('click', () => {
|
|
openNote(note.id);
|
|
});
|
|
|
|
noteList.appendChild(noteItem);
|
|
});
|
|
}
|
|
|
|
// Update word count
|
|
function updateWordCount() {
|
|
const text = editor.value.trim();
|
|
const words = text ? text.split(/\s+/).length : 0;
|
|
wordCount.textContent = `${words} word${words === 1 ? '' : 's'}`;
|
|
}
|
|
|
|
// Update last saved timestamp
|
|
function updateLastSaved(timestamp) {
|
|
if (!timestamp) {
|
|
lastSaved.textContent = 'Not saved yet';
|
|
return;
|
|
}
|
|
|
|
const date = new Date(timestamp);
|
|
lastSaved.textContent = `Last saved: ${date.toLocaleString()}`;
|
|
}
|
|
|
|
// Set theme
|
|
function setTheme(theme) {
|
|
document.body.className = '';
|
|
if (theme !== 'light') {
|
|
document.body.classList.add(`theme-${theme}`);
|
|
}
|
|
|
|
themeButtons.forEach(button => {
|
|
if (button.dataset.theme === theme) {
|
|
button.classList.add('active');
|
|
} else {
|
|
button.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
localStorage.setItem('theme', theme);
|
|
}
|
|
|
|
// Toggle sidebar (for mobile)
|
|
function toggleSidebar() {
|
|
sidebar.classList.toggle('open');
|
|
}
|
|
|
|
// Event listeners
|
|
editor.addEventListener('input', () => {
|
|
updateWordCount();
|
|
});
|
|
|
|
toggleSidebarBtn.addEventListener('click', toggleSidebar);
|
|
|
|
saveNoteBtn.addEventListener('click', saveCurrentNote);
|
|
downloadNoteBtn.addEventListener('click', downloadNoteAsMarkdown);
|
|
githubSaveBtn.addEventListener('click', saveToGithub);
|
|
deleteNoteBtn.addEventListener('click', deleteCurrentNote);
|
|
|
|
newNoteBtn.addEventListener('click', showNewNoteModal);
|
|
|
|
confirmGithubAuth.addEventListener('click', saveGithubAuth);
|
|
cancelGithubAuth.addEventListener('click', hideGithubModal);
|
|
|
|
confirmNewNote.addEventListener('click', () => {
|
|
const title = newNoteTitle.value.trim() || 'Untitled Note';
|
|
createNewNote(title);
|
|
hideNewNoteModal();
|
|
});
|
|
|
|
cancelNewNote.addEventListener('click', hideNewNoteModal);
|
|
|
|
// Theme selection
|
|
themeButtons.forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
setTheme(button.dataset.theme);
|
|
});
|
|
});
|
|
|
|
// Auto-save timer (every 30 seconds)
|
|
setInterval(() => {
|
|
if (currentNoteId && editor.value) {
|
|
saveCurrentNote();
|
|
}
|
|
}, 30000);
|
|
|
|
// Initialize
|
|
loadFromLocalStorage();
|
|
|
|
// Create first note if none exist
|
|
if (notes.length === 0) {
|
|
createNewNote('Welcome Note');
|
|
} else {
|
|
openNote(notes[0].id);
|
|
}
|
|
});
|
|
</script>
|
|
>>>>>>> c32bf888e7b226dce8ed1e85ec205b9386d99cf6
|
|
</body>
|
|
</html> |