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); | |
}); | |
} | |
}); |