Spaces:
Running
Running
| // Clock functionality | |
| function updateClock() { | |
| const now = new Date(); | |
| const timeElement = document.getElementById('time'); | |
| const dateElement = document.getElementById('date'); | |
| // Format time | |
| const hours = now.getHours().toString().padStart(2, '0'); | |
| const minutes = now.getMinutes().toString().padStart(2, '0'); | |
| const seconds = now.getSeconds().toString().padStart(2, '0'); | |
| timeElement.textContent = `${hours}:${minutes}:${seconds}`; | |
| // Format date | |
| const options = { | |
| weekday: 'long', | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }; | |
| const formattedDate = now.toLocaleDateString('en-US', options); | |
| // Add weather emoji | |
| const month = now.getMonth(); | |
| const hour = now.getHours(); | |
| let weatherEmoji = 'βοΈ'; | |
| if (month >= 11 || month <= 2) weatherEmoji = 'βοΈ'; | |
| else if (month >= 3 && month <= 5) weatherEmoji = 'πΈ'; | |
| else if (month >= 6 && month <= 8) weatherEmoji = 'βοΈ'; | |
| else weatherEmoji = 'π'; | |
| if (hour >= 19 || hour <= 6) weatherEmoji = 'π'; | |
| dateElement.textContent = `${formattedDate} ${weatherEmoji}`; | |
| } | |
| // Authentication functions | |
| async function logout() { | |
| try { | |
| await fetch('/logout', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| } | |
| }); | |
| window.location.href = '/'; | |
| } catch (error) { | |
| console.error('Logout failed:', error); | |
| window.location.href = '/'; | |
| } | |
| } | |
| // Search functionality | |
| function handleSearch() { | |
| const searchInput = document.getElementById('searchInput'); | |
| const query = searchInput.value.trim(); | |
| if (query) { | |
| // Track search usage | |
| fetch('/api/search', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| } | |
| }).catch(err => console.log('Search tracking failed:', err)); | |
| // Check if it's a URL | |
| const urlPattern = /^(https?:\/\/)?([\w\-])+\.{1}([a-zA-Z]{2,63})([\/\w\-\._~:?#[\]@!\$&'()*+,;=]*)?$/; | |
| if (urlPattern.test(query)) { | |
| // It's a URL, add https:// if not present | |
| const url = query.startsWith('http') ? query : `https://${query}`; | |
| window.open(url, '_blank'); | |
| } else { | |
| // It's a search query | |
| window.open(`https://www.google.com/search?q=${encodeURIComponent(query)}`, '_blank'); | |
| } | |
| searchInput.value = ''; | |
| } | |
| } | |
| // Default bookmarks | |
| const defaultBookmarks = [ | |
| { name: 'YouTube', url: 'https://youtube.com', icon: 'play_circle' }, | |
| { name: 'GitHub', url: 'https://github.com', icon: 'code' }, | |
| { name: 'Gmail', url: 'https://gmail.com', icon: 'mail' }, | |
| { name: 'Google Drive', url: 'https://drive.google.com', icon: 'cloud' }, | |
| { name: 'Netflix', url: 'https://netflix.com', icon: 'movie' }, | |
| { name: 'Reddit', url: 'https://reddit.com', icon: 'forum' }, | |
| { name: 'Twitter', url: 'https://twitter.com', icon: 'alternate_email' }, | |
| { name: 'LinkedIn', url: 'https://linkedin.com', icon: 'work' } | |
| ]; | |
| // Bookmarks management | |
| class BookmarksManager { | |
| constructor() { | |
| this.bookmarks = []; | |
| this.loadBookmarks(); | |
| } | |
| async loadBookmarks() { | |
| try { | |
| const response = await fetch('/api/bookmarks'); | |
| if (response.ok) { | |
| this.bookmarks = await response.json(); | |
| } else { | |
| // Fallback to default bookmarks if API fails | |
| this.bookmarks = defaultBookmarks; | |
| } | |
| } catch (error) { | |
| console.error('Failed to load bookmarks:', error); | |
| this.bookmarks = defaultBookmarks; | |
| } | |
| this.renderBookmarks(); | |
| } | |
| async saveBookmarks() { | |
| try { | |
| await fetch('/api/bookmarks', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ bookmarks: this.bookmarks }) | |
| }); | |
| } catch (error) { | |
| console.error('Failed to save bookmarks:', error); | |
| } | |
| } | |
| addBookmark(bookmark) { | |
| this.bookmarks.push(bookmark); | |
| this.saveBookmarks(); | |
| this.renderBookmarks(); | |
| } | |
| editBookmark(index, bookmark) { | |
| this.bookmarks[index] = bookmark; | |
| this.saveBookmarks(); | |
| this.renderBookmarks(); | |
| } | |
| removeBookmark(index) { | |
| this.bookmarks.splice(index, 1); | |
| this.saveBookmarks(); | |
| this.renderBookmarks(); | |
| } | |
| renderBookmarks() { | |
| const container = document.getElementById('bookmarksGrid'); | |
| container.innerHTML = ''; | |
| // Render existing bookmarks | |
| this.bookmarks.forEach((bookmark, index) => { | |
| const bookmarkElement = document.createElement('a'); | |
| bookmarkElement.className = 'bookmark'; | |
| bookmarkElement.href = bookmark.url; | |
| bookmarkElement.target = '_blank'; | |
| bookmarkElement.innerHTML = ` | |
| <span class="material-icons bookmark-icon">${bookmark.icon || 'bookmark'}</span> | |
| <span class="bookmark-name">${bookmark.name}</span> | |
| <span class="bookmark-url">${new URL(bookmark.url).hostname}</span> | |
| `; | |
| // Add context menu for editing | |
| bookmarkElement.addEventListener('contextmenu', (e) => { | |
| e.preventDefault(); | |
| this.openEditModal(index, bookmark); | |
| }); | |
| container.appendChild(bookmarkElement); | |
| }); | |
| // Add "Add Bookmark" button | |
| const addButton = document.createElement('div'); | |
| addButton.className = 'add-bookmark-btn'; | |
| addButton.innerHTML = ` | |
| <span class="material-icons">add</span> | |
| `; | |
| addButton.addEventListener('click', () => { | |
| this.openAddModal(); | |
| }); | |
| container.appendChild(addButton); | |
| } | |
| openEditModal(index, bookmark) { | |
| const modal = document.getElementById('bookmarkModal'); | |
| const modalTitle = document.getElementById('modalTitle'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const deleteBtn = document.getElementById('deleteBtn'); | |
| const nameInput = document.getElementById('bookmarkName'); | |
| const urlInput = document.getElementById('bookmarkUrl'); | |
| const iconSelect = document.getElementById('bookmarkIcon'); | |
| // Set modal to edit mode | |
| modalTitle.textContent = 'Edit Bookmark'; | |
| submitBtn.textContent = 'Update'; | |
| deleteBtn.style.display = 'block'; | |
| // Fill form with current bookmark data | |
| nameInput.value = bookmark.name; | |
| urlInput.value = bookmark.url; | |
| iconSelect.value = bookmark.icon || ''; | |
| // Store the index for editing | |
| modal.dataset.editIndex = index; | |
| modal.dataset.mode = 'edit'; | |
| // Set up delete button | |
| deleteBtn.onclick = () => { | |
| if (confirm(`Remove "${bookmark.name}" from bookmarks?`)) { | |
| this.removeBookmark(index); | |
| modal.style.display = 'none'; | |
| } | |
| }; | |
| modal.style.display = 'block'; | |
| } | |
| openAddModal() { | |
| const modal = document.getElementById('bookmarkModal'); | |
| const modalTitle = document.getElementById('modalTitle'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const deleteBtn = document.getElementById('deleteBtn'); | |
| const form = document.getElementById('bookmarkForm'); | |
| // Set modal to add mode | |
| modalTitle.textContent = 'Add Bookmark'; | |
| submitBtn.textContent = 'Add Bookmark'; | |
| deleteBtn.style.display = 'none'; | |
| // Clear form | |
| form.reset(); | |
| // Clear edit data | |
| delete modal.dataset.editIndex; | |
| modal.dataset.mode = 'add'; | |
| modal.style.display = 'block'; | |
| } | |
| } | |
| // Notepad functionality with multiple notes | |
| class NotePad { | |
| constructor() { | |
| this.notes = []; | |
| this.activeNoteId = 0; | |
| this.saveTimeouts = new Map(); | |
| this.loadNotes(); | |
| } | |
| async loadNotes() { | |
| try { | |
| const response = await fetch('/api/notes'); | |
| if (response.ok) { | |
| this.notes = await response.json(); | |
| if (this.notes.length === 0) { | |
| this.notes = [{ id: 0, title: 'Note 1', content: '' }]; | |
| } | |
| } else { | |
| this.notes = [{ id: 0, title: 'Note 1', content: '' }]; | |
| } | |
| } catch (error) { | |
| console.error('Failed to load notes:', error); | |
| this.notes = [{ id: 0, title: 'Note 1', content: '' }]; | |
| } | |
| this.renderNotes(); | |
| this.setupEventListeners(); | |
| } | |
| async saveNotes() { | |
| try { | |
| await fetch('/api/notes', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ notes: this.notes }) | |
| }); | |
| } catch (error) { | |
| console.error('Failed to save notes:', error); | |
| } | |
| } | |
| addNote() { | |
| const newId = Math.max(...this.notes.map(n => n.id), -1) + 1; | |
| const newNote = { | |
| id: newId, | |
| title: `Note ${newId + 1}`, | |
| content: '' | |
| }; | |
| this.notes.push(newNote); | |
| this.saveNotes(); | |
| this.renderNotes(); | |
| this.switchToNote(newId); | |
| } | |
| removeNote(noteId) { | |
| if (this.notes.length <= 1) { | |
| alert('Cannot delete the last note!'); | |
| return; | |
| } | |
| if (confirm('Are you sure you want to delete this note?')) { | |
| this.notes = this.notes.filter(note => note.id !== noteId); | |
| // If we deleted the active note, switch to the first available note | |
| if (this.activeNoteId === noteId) { | |
| this.activeNoteId = this.notes[0].id; | |
| } | |
| this.saveNotes(); | |
| this.renderNotes(); | |
| } | |
| } | |
| switchToNote(noteId) { | |
| // Save current note content first | |
| this.saveCurrentNote(); | |
| this.activeNoteId = noteId; | |
| this.renderNotes(); | |
| // Focus on the new note's textarea | |
| const activeTextarea = document.querySelector('.note-content.active .notepad-textarea'); | |
| if (activeTextarea) { | |
| activeTextarea.focus(); | |
| } | |
| } | |
| saveCurrentNote() { | |
| const activeTextarea = document.querySelector('.note-content.active .notepad-textarea'); | |
| if (activeTextarea) { | |
| const note = this.notes.find(n => n.id === this.activeNoteId); | |
| if (note) { | |
| note.content = activeTextarea.value; | |
| this.saveNotes(); | |
| } | |
| } | |
| } | |
| showSaveIndicator(noteId) { | |
| const saveIndicator = document.querySelector(`[data-note-id="${noteId}"] .save-indicator`); | |
| if (saveIndicator) { | |
| saveIndicator.classList.add('visible'); | |
| setTimeout(() => { | |
| saveIndicator.classList.remove('visible'); | |
| }, 1500); | |
| } | |
| } | |
| renderNotes() { | |
| const tabsContainer = document.querySelector('.notes-tabs'); | |
| const notesContainer = document.querySelector('.notepad-container'); | |
| // Clear existing tabs (except add button) | |
| const addButton = tabsContainer.querySelector('.add-note-btn'); | |
| tabsContainer.innerHTML = ''; | |
| // Render note tabs | |
| this.notes.forEach(note => { | |
| const tab = document.createElement('button'); | |
| tab.className = `note-tab ${note.id === this.activeNoteId ? 'active' : ''}`; | |
| tab.setAttribute('data-note-id', note.id); | |
| tab.innerHTML = ` | |
| ${note.title} | |
| ${this.notes.length > 1 ? `<span class="close-note" onclick="event.stopPropagation(); window.notePad.removeNote(${note.id});">Γ</span>` : ''} | |
| `; | |
| tab.addEventListener('click', () => this.switchToNote(note.id)); | |
| tabsContainer.appendChild(tab); | |
| }); | |
| // Re-add the add button | |
| tabsContainer.appendChild(addButton); | |
| // Clear and render note contents | |
| const existingContents = notesContainer.querySelectorAll('.note-content'); | |
| existingContents.forEach(content => content.remove()); | |
| this.notes.forEach(note => { | |
| const noteContent = document.createElement('div'); | |
| noteContent.className = `note-content ${note.id === this.activeNoteId ? 'active' : ''}`; | |
| noteContent.setAttribute('data-note-id', note.id); | |
| noteContent.innerHTML = ` | |
| <textarea class="notepad-textarea" placeholder="Start typing your notes here...">${note.content}</textarea> | |
| <div class="notepad-footer"> | |
| <span class="save-indicator"> | |
| <span class="material-icons" style="font-size: 1rem; vertical-align: middle;">check_circle</span> | |
| Saved | |
| </span> | |
| </div> | |
| `; | |
| notesContainer.appendChild(noteContent); | |
| }); | |
| this.setupNoteEventListeners(); | |
| } | |
| setupEventListeners() { | |
| // Global event listeners can be set up here | |
| } | |
| setupNoteEventListeners() { | |
| this.notes.forEach(note => { | |
| const textarea = document.querySelector(`[data-note-id="${note.id}"] .notepad-textarea`); | |
| if (textarea) { | |
| // Remove existing listeners to avoid duplicates | |
| textarea.removeEventListener('input', this.handleInput); | |
| textarea.removeEventListener('blur', this.handleBlur); | |
| // Add new listeners | |
| textarea.addEventListener('input', () => { | |
| // Update note content | |
| const noteData = this.notes.find(n => n.id === note.id); | |
| if (noteData) { | |
| noteData.content = textarea.value; | |
| } | |
| // Auto-save with debounce | |
| if (this.saveTimeouts.has(note.id)) { | |
| clearTimeout(this.saveTimeouts.get(note.id)); | |
| } | |
| const timeout = setTimeout(() => { | |
| this.saveNotes(); | |
| this.showSaveIndicator(note.id); | |
| }, 1000); | |
| this.saveTimeouts.set(note.id, timeout); | |
| }); | |
| textarea.addEventListener('blur', () => { | |
| if (this.saveTimeouts.has(note.id)) { | |
| clearTimeout(this.saveTimeouts.get(note.id)); | |
| } | |
| this.saveCurrentNote(); | |
| this.showSaveIndicator(note.id); | |
| }); | |
| } | |
| }); | |
| } | |
| } | |
| // Modal functionality | |
| class Modal { | |
| constructor() { | |
| this.modal = document.getElementById('bookmarkModal'); | |
| this.form = document.getElementById('bookmarkForm'); | |
| this.closeBtn = document.getElementById('closeModal'); | |
| this.cancelBtn = document.getElementById('cancelBtn'); | |
| this.setupEventListeners(); | |
| } | |
| setupEventListeners() { | |
| // Close modal events | |
| this.closeBtn.addEventListener('click', () => this.close()); | |
| this.cancelBtn.addEventListener('click', () => this.close()); | |
| // Close on outside click | |
| window.addEventListener('click', (e) => { | |
| if (e.target === this.modal) { | |
| this.close(); | |
| } | |
| }); | |
| // Form submission | |
| this.form.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| this.handleSubmit(); | |
| }); | |
| // Close on Escape key | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape' && this.modal.style.display === 'block') { | |
| this.close(); | |
| } | |
| }); | |
| } | |
| open() { | |
| this.modal.style.display = 'block'; | |
| document.getElementById('bookmarkName').focus(); | |
| } | |
| close() { | |
| this.modal.style.display = 'none'; | |
| this.form.reset(); | |
| // Clear edit data | |
| delete this.modal.dataset.editIndex; | |
| delete this.modal.dataset.mode; | |
| // Reset modal to add mode | |
| document.getElementById('modalTitle').textContent = 'Add Bookmark'; | |
| document.getElementById('submitBtn').textContent = 'Add Bookmark'; | |
| document.getElementById('deleteBtn').style.display = 'none'; | |
| } | |
| handleSubmit() { | |
| const name = document.getElementById('bookmarkName').value.trim(); | |
| const url = document.getElementById('bookmarkUrl').value.trim(); | |
| const icon = document.getElementById('bookmarkIcon').value.trim() || 'bookmark'; | |
| if (name && url) { | |
| // Ensure URL has protocol | |
| const finalUrl = url.startsWith('http') ? url : `https://${url}`; | |
| const bookmark = { | |
| name, | |
| url: finalUrl, | |
| icon | |
| }; | |
| // Check if we're in edit mode | |
| if (this.modal.dataset.mode === 'edit') { | |
| const editIndex = parseInt(this.modal.dataset.editIndex); | |
| bookmarksManager.editBookmark(editIndex, bookmark); | |
| } else { | |
| bookmarksManager.addBookmark(bookmark); | |
| } | |
| this.close(); | |
| } | |
| } | |
| } | |
| // Theme and settings removed - using white background only | |
| // Initialize everything when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Initialize clock | |
| updateClock(); | |
| setInterval(updateClock, 1000); | |
| // Initialize components | |
| window.bookmarksManager = new BookmarksManager(); | |
| window.notePad = new NotePad(); | |
| window.modal = new Modal(); | |
| // Setup search functionality | |
| const searchInput = document.getElementById('searchInput'); | |
| const searchIcon = document.getElementById('searchIcon'); | |
| searchInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| handleSearch(); | |
| } | |
| }); | |
| searchIcon.addEventListener('click', handleSearch); | |
| // Focus search on load | |
| searchInput.focus(); | |
| // Display username (passed from Flask template) | |
| const usernameElement = document.getElementById('username'); | |
| if (usernameElement && typeof window.currentUser !== 'undefined') { | |
| usernameElement.textContent = window.currentUser; | |
| } | |
| }); | |
| // Service Worker for offline capability (future enhancement) | |
| if ('serviceWorker' in navigator) { | |
| window.addEventListener('load', () => { | |
| // Service worker registration could be added here | |
| console.log('Start page loaded successfully!'); | |
| }); | |
| } | |
| // Export for potential future use | |
| window.StartPageAPI = { | |
| bookmarksManager: () => window.bookmarksManager, | |
| notePad: () => window.notePad, | |
| addBookmark: (bookmark) => window.bookmarksManager.addBookmark(bookmark), | |
| exportData: () => ({ | |
| bookmarks: window.bookmarksManager.bookmarks, | |
| notes: window.notePad.notes | |
| }), | |
| importData: (data) => { | |
| if (data.bookmarks) { | |
| localStorage.setItem('startpage-bookmarks', JSON.stringify(data.bookmarks)); | |
| } | |
| if (data.notes) { | |
| localStorage.setItem('startpage-notes', JSON.stringify(data.notes)); | |
| } | |
| location.reload(); | |
| } | |
| }; | |
| // Rain Effect | |
| class RainEffect { | |
| constructor() { | |
| this.rainContainer = document.getElementById('rainContainer'); | |
| this.rainDrops = []; | |
| this.maxDrops = 150; | |
| this.init(); | |
| } | |
| init() { | |
| this.createRain(); | |
| this.animateRain(); | |
| } | |
| createRain() { | |
| for (let i = 0; i < this.maxDrops; i++) { | |
| this.createRainDrop(); | |
| } | |
| } | |
| createRainDrop() { | |
| const drop = document.createElement('div'); | |
| drop.className = 'rain-drop'; | |
| // Random horizontal position | |
| const x = Math.random() * window.innerWidth; | |
| // Random size variation | |
| const size = Math.random() * 0.8 + 0.2; | |
| drop.style.width = `${2 * size}px`; | |
| drop.style.height = `${20 * size}px`; | |
| // Random speed (duration) | |
| const duration = Math.random() * 2 + 1; // 1-3 seconds | |
| drop.style.animationDuration = `${duration}s`; | |
| // Random delay | |
| const delay = Math.random() * 2; | |
| drop.style.animationDelay = `${delay}s`; | |
| // Position the drop | |
| drop.style.left = `${x}px`; | |
| drop.style.top = '-20px'; | |
| this.rainContainer.appendChild(drop); | |
| this.rainDrops.push(drop); | |
| } | |
| animateRain() { | |
| // Clean up and recreate drops periodically | |
| setInterval(() => { | |
| this.rainDrops.forEach(drop => { | |
| const rect = drop.getBoundingClientRect(); | |
| if (rect.top > window.innerHeight) { | |
| // Reset the drop to the top with new random position | |
| drop.style.left = `${Math.random() * window.innerWidth}px`; | |
| drop.style.top = '-20px'; | |
| // Randomize properties again | |
| const size = Math.random() * 0.8 + 0.2; | |
| drop.style.width = `${2 * size}px`; | |
| drop.style.height = `${20 * size}px`; | |
| const duration = Math.random() * 2 + 1; | |
| drop.style.animationDuration = `${duration}s`; | |
| } | |
| }); | |
| }, 100); | |
| } | |
| } | |
| // Initialize rain effect when the page loads | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new RainEffect(); | |
| }); | |
| // Account dropdown functionality | |
| function toggleAccountDropdown() { | |
| const dropdown = document.getElementById('accountDropdown'); | |
| dropdown.classList.toggle('active'); | |
| } | |
| // Close dropdown when clicking outside | |
| document.addEventListener('click', (e) => { | |
| const dropdown = document.getElementById('accountDropdown'); | |
| const accountBtn = document.querySelector('.account-btn'); | |
| if (!accountBtn.contains(e.target) && !dropdown.contains(e.target)) { | |
| dropdown.classList.remove('active'); | |
| } | |
| }); | |
| // Change Password Modal Functions | |
| function openChangePasswordModal() { | |
| const modal = document.getElementById('passwordModal'); | |
| modal.style.display = 'block'; | |
| document.getElementById('accountDropdown').classList.remove('active'); | |
| } | |
| function closePasswordModal() { | |
| const modal = document.getElementById('passwordModal'); | |
| modal.style.display = 'none'; | |
| document.getElementById('passwordForm').reset(); | |
| } | |
| // Handle password form submission | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const passwordForm = document.getElementById('passwordForm'); | |
| if (passwordForm) { | |
| passwordForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const currentPassword = document.getElementById('currentPassword').value; | |
| const newPassword = document.getElementById('newPassword').value; | |
| const confirmPassword = document.getElementById('confirmPassword').value; | |
| if (newPassword !== confirmPassword) { | |
| alert('New passwords do not match!'); | |
| return; | |
| } | |
| if (newPassword.length < 6) { | |
| alert('Password must be at least 6 characters long!'); | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/api/change-password', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| currentPassword, | |
| newPassword | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| alert('Password changed successfully!'); | |
| closePasswordModal(); | |
| } else { | |
| alert(data.error || 'Failed to change password'); | |
| } | |
| } catch (error) { | |
| console.error('Error changing password:', error); | |
| alert('Failed to change password. Please try again.'); | |
| } | |
| }); | |
| } | |
| }); | |
| // Developer Stats Modal Functions | |
| function openDeveloperPage() { | |
| const modal = document.getElementById('developerModal'); | |
| modal.style.display = 'block'; | |
| document.getElementById('accountDropdown').classList.remove('active'); | |
| loadDeveloperStats(); | |
| } | |
| function closeDeveloperModal() { | |
| const modal = document.getElementById('developerModal'); | |
| modal.style.display = 'none'; | |
| // Destroy chart to prevent memory leaks | |
| if (activityChart) { | |
| activityChart.destroy(); | |
| activityChart = null; | |
| } | |
| } | |
| async function loadDeveloperStats() { | |
| try { | |
| const response = await fetch('/api/developer-stats'); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| // Update stats display | |
| document.getElementById('totalUsers').textContent = data.totalUsers || 0; | |
| document.getElementById('totalVisits').textContent = data.totalVisits || 0; | |
| document.getElementById('totalSearches').textContent = data.totalSearches || 0; | |
| // Create chart | |
| createActivityChart(data.monthlyData || []); | |
| } else { | |
| console.error('Failed to load developer stats'); | |
| } | |
| } catch (error) { | |
| console.error('Error loading developer stats:', error); | |
| } | |
| } | |
| let activityChart = null; // Global variable to store chart instance | |
| function createActivityChart(monthlyData) { | |
| const canvas = document.getElementById('activityChart'); | |
| const ctx = canvas.getContext('2d'); | |
| // Destroy existing chart if it exists | |
| if (activityChart) { | |
| activityChart.destroy(); | |
| } | |
| // Generate sample monthly data if none provided | |
| if (monthlyData.length === 0) { | |
| const currentDate = new Date(); | |
| monthlyData = []; | |
| for (let i = 11; i >= 0; i--) { | |
| const date = new Date(currentDate.getFullYear(), currentDate.getMonth() - i, 1); | |
| const monthName = date.toLocaleString('default', { month: 'short' }); | |
| monthlyData.push({ | |
| month: monthName, | |
| visits: Math.floor(Math.random() * 500) + 200, | |
| searches: Math.floor(Math.random() * 300) + 100 | |
| }); | |
| } | |
| } | |
| // Extract data for Chart.js | |
| const labels = monthlyData.map(data => data.month); | |
| const visitsData = monthlyData.map(data => data.visits); | |
| const searchesData = monthlyData.map(data => data.searches); | |
| // Create Chart.js line chart | |
| activityChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: labels, | |
| datasets: [ | |
| { | |
| label: 'Page Visits', | |
| data: visitsData, | |
| borderColor: '#4285f4', | |
| backgroundColor: 'rgba(66, 133, 244, 0.1)', | |
| borderWidth: 3, | |
| fill: true, | |
| tension: 0.4, | |
| pointBackgroundColor: '#4285f4', | |
| pointBorderColor: '#4285f4', | |
| pointHoverBackgroundColor: '#ffffff', | |
| pointHoverBorderColor: '#4285f4', | |
| pointRadius: 5, | |
| pointHoverRadius: 7 | |
| }, | |
| { | |
| label: 'Searches', | |
| data: searchesData, | |
| borderColor: '#34a853', | |
| backgroundColor: 'rgba(52, 168, 83, 0.1)', | |
| borderWidth: 3, | |
| fill: true, | |
| tension: 0.4, | |
| pointBackgroundColor: '#34a853', | |
| pointBorderColor: '#34a853', | |
| pointHoverBackgroundColor: '#ffffff', | |
| pointHoverBorderColor: '#34a853', | |
| pointRadius: 5, | |
| pointHoverRadius: 7 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: true, | |
| position: 'top', | |
| labels: { | |
| usePointStyle: true, | |
| padding: 20, | |
| font: { | |
| size: 12, | |
| family: 'Lexend, sans-serif' | |
| } | |
| } | |
| }, | |
| tooltip: { | |
| mode: 'index', | |
| intersect: false, | |
| backgroundColor: 'rgba(0, 0, 0, 0.8)', | |
| titleColor: '#ffffff', | |
| bodyColor: '#ffffff', | |
| borderColor: '#4285f4', | |
| borderWidth: 1, | |
| cornerRadius: 8, | |
| displayColors: true, | |
| titleFont: { | |
| size: 14, | |
| weight: 'bold' | |
| }, | |
| bodyFont: { | |
| size: 13 | |
| } | |
| } | |
| }, | |
| interaction: { | |
| mode: 'nearest', | |
| axis: 'x', | |
| intersect: false | |
| }, | |
| scales: { | |
| x: { | |
| display: true, | |
| title: { | |
| display: true, | |
| text: 'Month', | |
| font: { | |
| size: 14, | |
| weight: 'bold', | |
| family: 'Lexend, sans-serif' | |
| }, | |
| color: '#666' | |
| }, | |
| grid: { | |
| display: true, | |
| color: 'rgba(0, 0, 0, 0.1)' | |
| }, | |
| ticks: { | |
| font: { | |
| size: 12, | |
| family: 'Lexend, sans-serif' | |
| }, | |
| color: '#666' | |
| } | |
| }, | |
| y: { | |
| display: true, | |
| title: { | |
| display: true, | |
| text: 'Count', | |
| font: { | |
| size: 14, | |
| weight: 'bold', | |
| family: 'Lexend, sans-serif' | |
| }, | |
| color: '#666' | |
| }, | |
| grid: { | |
| display: true, | |
| color: 'rgba(0, 0, 0, 0.1)' | |
| }, | |
| ticks: { | |
| beginAtZero: true, | |
| font: { | |
| size: 12, | |
| family: 'Lexend, sans-serif' | |
| }, | |
| color: '#666' | |
| } | |
| } | |
| }, | |
| elements: { | |
| line: { | |
| borderJoinStyle: 'round' | |
| }, | |
| point: { | |
| borderWidth: 2, | |
| hoverBorderWidth: 3 | |
| } | |
| }, | |
| animation: { | |
| duration: 1500, | |
| easing: 'easeInOutQuart' | |
| } | |
| } | |
| }); | |
| } | |
| // Close modals on Escape key | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| const passwordModal = document.getElementById('passwordModal'); | |
| const developerModal = document.getElementById('developerModal'); | |
| const vaultAuthModal = document.getElementById('vaultAuthModal'); | |
| const vaultModal = document.getElementById('vaultModal'); | |
| const passwordEntryModal = document.getElementById('passwordEntryModal'); | |
| if (passwordModal.style.display === 'block') { | |
| closePasswordModal(); | |
| } | |
| if (developerModal.style.display === 'block') { | |
| closeDeveloperModal(); | |
| } | |
| if (vaultAuthModal.style.display === 'block') { | |
| closeVaultAuthModal(); | |
| } | |
| if (vaultModal.style.display === 'block') { | |
| closeVaultModal(); | |
| } | |
| if (passwordEntryModal.style.display === 'block') { | |
| closePasswordEntryModal(); | |
| } | |
| } | |
| }); | |
| // Close modals when clicking outside | |
| window.addEventListener('click', (e) => { | |
| const passwordModal = document.getElementById('passwordModal'); | |
| const developerModal = document.getElementById('developerModal'); | |
| const vaultAuthModal = document.getElementById('vaultAuthModal'); | |
| const vaultModal = document.getElementById('vaultModal'); | |
| const passwordEntryModal = document.getElementById('passwordEntryModal'); | |
| if (e.target === passwordModal) { | |
| closePasswordModal(); | |
| } | |
| if (e.target === developerModal) { | |
| closeDeveloperModal(); | |
| } | |
| if (e.target === vaultAuthModal) { | |
| closeVaultAuthModal(); | |
| } | |
| if (e.target === vaultModal) { | |
| closeVaultModal(); | |
| } | |
| if (e.target === passwordEntryModal) { | |
| closePasswordEntryModal(); | |
| } | |
| }); | |
| // Vault functionality | |
| let vaultPasswords = []; | |
| let currentEditingPassword = null; | |
| function openVaultPage() { | |
| document.getElementById('accountDropdown').classList.remove('active'); | |
| document.getElementById('vaultAuthModal').style.display = 'block'; | |
| } | |
| function closeVaultAuthModal() { | |
| document.getElementById('vaultAuthModal').style.display = 'none'; | |
| document.getElementById('vaultAuthForm').reset(); | |
| } | |
| function openVaultModal() { | |
| document.getElementById('vaultModal').style.display = 'block'; | |
| loadVaultPasswords(); | |
| } | |
| function closeVaultModal() { | |
| document.getElementById('vaultModal').style.display = 'none'; | |
| vaultPasswords = []; | |
| renderVaultPasswords(); | |
| } | |
| function openAddPasswordModal() { | |
| currentEditingPassword = null; | |
| document.getElementById('passwordEntryTitle').textContent = 'Add Password'; | |
| document.getElementById('savePasswordBtn').textContent = 'Save Password'; | |
| document.getElementById('deletePasswordBtn').style.display = 'none'; | |
| document.getElementById('passwordEntryForm').reset(); | |
| document.getElementById('passwordEntryModal').style.display = 'block'; | |
| } | |
| function openEditPasswordModal(index) { | |
| currentEditingPassword = index; | |
| const password = vaultPasswords[index]; | |
| document.getElementById('passwordEntryTitle').textContent = 'Edit Password'; | |
| document.getElementById('savePasswordBtn').textContent = 'Update Password'; | |
| document.getElementById('deletePasswordBtn').style.display = 'block'; | |
| document.getElementById('entryTitle').value = password.title; | |
| document.getElementById('entryUsername').value = password.username || ''; | |
| document.getElementById('entryPassword').value = password.password; | |
| document.getElementById('entryWebsite').value = password.website || ''; | |
| document.getElementById('entryNotes').value = password.notes || ''; | |
| document.getElementById('passwordEntryModal').style.display = 'block'; | |
| } | |
| function closePasswordEntryModal() { | |
| document.getElementById('passwordEntryModal').style.display = 'none'; | |
| document.getElementById('passwordEntryForm').reset(); | |
| currentEditingPassword = null; | |
| } | |
| async function loadVaultPasswords() { | |
| try { | |
| const response = await fetch('/api/vault/passwords'); | |
| if (response.ok) { | |
| vaultPasswords = await response.json(); | |
| } else { | |
| vaultPasswords = []; | |
| } | |
| renderVaultPasswords(); | |
| } catch (error) { | |
| console.error('Failed to load vault passwords:', error); | |
| vaultPasswords = []; | |
| renderVaultPasswords(); | |
| } | |
| } | |
| async function saveVaultPassword(passwordData) { | |
| try { | |
| const response = await fetch('/api/vault/passwords', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(passwordData) | |
| }); | |
| if (response.ok) { | |
| loadVaultPasswords(); | |
| return true; | |
| } else { | |
| console.error('Failed to save password'); | |
| return false; | |
| } | |
| } catch (error) { | |
| console.error('Failed to save password:', error); | |
| return false; | |
| } | |
| } | |
| async function deleteVaultPassword(index) { | |
| try { | |
| const password = vaultPasswords[index]; | |
| const response = await fetch(`/api/vault/passwords/${password.id}`, { | |
| method: 'DELETE' | |
| }); | |
| if (response.ok) { | |
| loadVaultPasswords(); | |
| return true; | |
| } else { | |
| console.error('Failed to delete password'); | |
| return false; | |
| } | |
| } catch (error) { | |
| console.error('Failed to delete password:', error); | |
| return false; | |
| } | |
| } | |
| function renderVaultPasswords() { | |
| const vaultList = document.getElementById('vaultList'); | |
| const searchTerm = document.getElementById('vaultSearchInput').value.toLowerCase(); | |
| // Filter passwords based on search term | |
| const filteredPasswords = vaultPasswords.filter(password => | |
| password.title.toLowerCase().includes(searchTerm) || | |
| (password.username && password.username.toLowerCase().includes(searchTerm)) || | |
| (password.website && password.website.toLowerCase().includes(searchTerm)) || | |
| (password.notes && password.notes.toLowerCase().includes(searchTerm)) | |
| ); | |
| if (filteredPasswords.length === 0) { | |
| vaultList.innerHTML = ` | |
| <div class="vault-empty"> | |
| <div class="material-icons">lock</div> | |
| <p>${searchTerm ? 'No passwords found matching your search.' : 'Your vault is empty. Add your first password!'}</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| vaultList.innerHTML = filteredPasswords.map((password, originalIndex) => { | |
| const index = vaultPasswords.indexOf(password); | |
| return ` | |
| <div class="vault-entry" onclick="openEditPasswordModal(${index})"> | |
| <div class="vault-entry-header"> | |
| <div class="vault-entry-title">${password.title}</div> | |
| <div class="vault-entry-actions" onclick="event.stopPropagation();"> | |
| <button class="vault-action-btn" onclick="copyToClipboard('${password.password}', 'Password')" title="Copy Password"> | |
| <span class="material-icons">content_copy</span> | |
| </button> | |
| <button class="vault-action-btn" onclick="openEditPasswordModal(${index})" title="Edit"> | |
| <span class="material-icons">edit</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="vault-entry-info"> | |
| ${password.username ? ` | |
| <span class="vault-entry-label">Username:</span> | |
| <span class="vault-entry-value">${password.username}</span> | |
| ` : ''} | |
| ${password.website ? ` | |
| <span class="vault-entry-label">Website:</span> | |
| <span class="vault-entry-value">${password.website}</span> | |
| ` : ''} | |
| ${password.notes ? ` | |
| <span class="vault-entry-label">Notes:</span> | |
| <span class="vault-entry-value">${password.notes}</span> | |
| ` : ''} | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function copyToClipboard(text, type) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| // Show temporary success message | |
| const message = document.createElement('div'); | |
| message.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| background: #4caf50; | |
| color: white; | |
| padding: 12px 20px; | |
| border-radius: 8px; | |
| z-index: 10000; | |
| font-size: 14px; | |
| `; | |
| message.textContent = `${type} copied to clipboard!`; | |
| document.body.appendChild(message); | |
| setTimeout(() => { | |
| document.body.removeChild(message); | |
| }, 2000); | |
| }).catch(() => { | |
| alert('Failed to copy to clipboard'); | |
| }); | |
| } | |
| function togglePasswordVisibility(inputId) { | |
| const input = document.getElementById(inputId); | |
| const toggleBtn = input.parentNode.querySelector('.password-toggle-btn span'); | |
| if (input.type === 'password') { | |
| input.type = 'text'; | |
| toggleBtn.textContent = 'visibility_off'; | |
| } else { | |
| input.type = 'password'; | |
| toggleBtn.textContent = 'visibility'; | |
| } | |
| } | |
| function generatePassword() { | |
| const length = 16; | |
| const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*'; | |
| let password = ''; | |
| for (let i = 0; i < length; i++) { | |
| password += charset.charAt(Math.floor(Math.random() * charset.length)); | |
| } | |
| document.getElementById('entryPassword').value = password; | |
| updatePasswordStrength(password); | |
| } | |
| function updatePasswordStrength(password) { | |
| const strengthIndicator = document.querySelector('.password-strength-bar'); | |
| if (!strengthIndicator) return; | |
| let strength = 0; | |
| if (password.length >= 8) strength++; | |
| if (/[a-z]/.test(password)) strength++; | |
| if (/[A-Z]/.test(password)) strength++; | |
| if (/[0-9]/.test(password)) strength++; | |
| if (/[^A-Za-z0-9]/.test(password)) strength++; | |
| const classes = ['strength-weak', 'strength-fair', 'strength-good', 'strength-strong']; | |
| strengthIndicator.className = 'password-strength-bar ' + (classes[Math.min(strength - 1, 3)] || ''); | |
| } | |
| // Initialize vault event listeners | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Vault authentication form | |
| const vaultAuthForm = document.getElementById('vaultAuthForm'); | |
| if (vaultAuthForm) { | |
| vaultAuthForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const password = document.getElementById('vaultPassword').value; | |
| try { | |
| const response = await fetch('/api/vault/authenticate', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ password }) | |
| }); | |
| if (response.ok) { | |
| closeVaultAuthModal(); | |
| openVaultModal(); | |
| } else { | |
| const data = await response.json(); | |
| alert(data.error || 'Invalid password'); | |
| } | |
| } catch (error) { | |
| console.error('Vault authentication failed:', error); | |
| alert('Authentication failed. Please try again.'); | |
| } | |
| }); | |
| } | |
| // Password entry form | |
| const passwordEntryForm = document.getElementById('passwordEntryForm'); | |
| if (passwordEntryForm) { | |
| passwordEntryForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const passwordData = { | |
| title: document.getElementById('entryTitle').value, | |
| username: document.getElementById('entryUsername').value, | |
| password: document.getElementById('entryPassword').value, | |
| website: document.getElementById('entryWebsite').value, | |
| notes: document.getElementById('entryNotes').value | |
| }; | |
| if (currentEditingPassword !== null) { | |
| passwordData.id = vaultPasswords[currentEditingPassword].id; | |
| } | |
| const success = await saveVaultPassword(passwordData); | |
| if (success) { | |
| closePasswordEntryModal(); | |
| } else { | |
| alert('Failed to save password. Please try again.'); | |
| } | |
| }); | |
| } | |
| // Delete password button | |
| const deletePasswordBtn = document.getElementById('deletePasswordBtn'); | |
| if (deletePasswordBtn) { | |
| deletePasswordBtn.addEventListener('click', async () => { | |
| if (currentEditingPassword !== null) { | |
| if (confirm('Are you sure you want to delete this password?')) { | |
| const success = await deleteVaultPassword(currentEditingPassword); | |
| if (success) { | |
| closePasswordEntryModal(); | |
| } else { | |
| alert('Failed to delete password. Please try again.'); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Vault search functionality | |
| const vaultSearchInput = document.getElementById('vaultSearchInput'); | |
| if (vaultSearchInput) { | |
| vaultSearchInput.addEventListener('input', () => { | |
| renderVaultPasswords(); | |
| }); | |
| } | |
| // Password strength indicator | |
| const entryPasswordInput = document.getElementById('entryPassword'); | |
| if (entryPasswordInput) { | |
| entryPasswordInput.addEventListener('input', (e) => { | |
| updatePasswordStrength(e.target.value); | |
| }); | |
| } | |
| }); |