diff --git a/index.html b/index.html new file mode 100644 index 0000000..6423a0d --- /dev/null +++ b/index.html @@ -0,0 +1,271 @@ + + + + + + New JSTAR Tab + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..65f7023 --- /dev/null +++ b/js/main.js @@ -0,0 +1,98 @@ +// Update greeting based on time of day and user settings +function updateGreeting() { + const greeting = document.getElementById('greeting'); + if (!greeting) return; + + const hour = new Date().getHours(); + const isAnonymous = Storage.get('anonymousMode') || false; + const userName = isAnonymous ? + (Storage.get('anonymousName') || anonymousNames.generate()) : + (Storage.get('userName') || 'Friend'); + + let timeGreeting = 'Hello'; + if (hour >= 5 && hour < 12) timeGreeting = 'Good Morning'; + else if (hour >= 12 && hour < 17) timeGreeting = 'Good Afternoon'; + else if (hour >= 17 && hour < 20) timeGreeting = 'Good Evening'; + else timeGreeting = 'Good Night'; + + greeting.textContent = `${timeGreeting}, ${userName}!`; + greeting.style.opacity = '0'; + setTimeout(() => { + greeting.style.opacity = '1'; + }, 100); +} + +// Set up event listeners for modal interactions +function initModalHandlers() { + const modals = document.querySelectorAll('.modal'); + + modals.forEach(modal => { + modal.addEventListener('click', (e) => { + if (e.target === modal) { + closeModal(modal); + } + }); + + const modalContent = modal.querySelector('.modal-content'); + if (modalContent) { + modalContent.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } + }); +} + +// Open modal with animation +function openModal(modal) { + if (!modal) return; + modal.classList.remove('hidden'); + requestAnimationFrame(() => { + modal.classList.add('active'); + }); +} + +// Close modal with animation +function closeModal(modal) { + if (!modal) return; + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); +} + +// Initialize application +document.addEventListener('DOMContentLoaded', () => { + // Apply visibility settings + ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { + const isVisible = Storage.get(`show_${element}`); + if (isVisible === false) { + const elementNode = document.getElementById(element === 'search' ? 'search-container' : element); + if (elementNode) elementNode.style.display = 'none'; + } + }); + + // Start onboarding or show main content + if (!Storage.get('onboardingComplete')) { + onboarding.start(); + } else { + document.getElementById('main-content').classList.remove('hidden'); + } + + // Initialize features + search.init(); + shortcuts.init(); + settings.init(); + initModalHandlers(); + + // Set up greeting + updateGreeting(); + setInterval(updateGreeting, 60000); + + // Settings button handler + const settingsButton = document.getElementById('settings-button'); + const settingsModal = document.getElementById('settings-modal'); + + settingsButton.addEventListener('click', () => { + openModal(settingsModal); + }); +}); \ No newline at end of file diff --git a/js/notifications.js b/js/notifications.js new file mode 100644 index 0000000..0a2b762 --- /dev/null +++ b/js/notifications.js @@ -0,0 +1,131 @@ +// Notification System Class +class NotificationSystem { + constructor() { + this.container = document.getElementById('notification-container'); + this.notifications = new Map(); + } + + // Display a new notification + show(message, type = 'info', duration = 3000) { + const id = Date.now().toString(); + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + + // Create notification elements + const icon = this.createIcon(type); + const content = this.createContent(message); + const closeBtn = this.createCloseButton(id); + const progress = this.createProgressBar(type); + + // Assemble notification + notification.appendChild(icon); + notification.appendChild(content); + notification.appendChild(closeBtn); + notification.appendChild(progress); + + this.container.appendChild(notification); + + // Set removal timer + setTimeout(() => this.remove(id), duration); + + // Store notification reference + this.notifications.set(id, { + element: notification, + duration + }); + + this.updateProgress(id); + + return id; + } + + // Remove a notification + remove(id) { + const notification = this.notifications.get(id); + if (notification) { + notification.element.style.animation = 'slideOutRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)'; + setTimeout(() => { + notification.element.remove(); + this.notifications.delete(id); + }, 300); + } + } + + // Update progress bar + updateProgress(id) { + const notification = this.notifications.get(id); + if (notification) { + const progress = notification.element.querySelector('.notification-progress'); + const startTime = Date.now(); + + const update = () => { + const elapsed = Date.now() - startTime; + const percent = 100 - (elapsed / notification.duration * 100); + + if (percent > 0) { + progress.style.width = `${percent}%`; + requestAnimationFrame(update); + } + }; + + requestAnimationFrame(update); + } + } + + // Helper methods for creating notification elements + createIcon(type) { + const icon = document.createElement('i'); + switch(type) { + case 'success': + icon.className = 'fas fa-check-circle'; + icon.style.color = 'var(--success-color, #4caf50)'; + break; + case 'error': + icon.className = 'fas fa-times-circle'; + icon.style.color = 'var(--error-color, #f44336)'; + break; + case 'info': + default: + icon.className = 'fas fa-info-circle'; + icon.style.color = 'var(--info-color, #2196f3)'; + break; + } + return icon; + } + + createContent(message) { + const content = document.createElement('div'); + content.className = 'notification-content'; + content.textContent = message; + return content; + } + + createCloseButton(id) { + const closeBtn = document.createElement('button'); + closeBtn.className = 'notification-close'; + closeBtn.innerHTML = ''; + closeBtn.onclick = () => this.remove(id); + return closeBtn; + } + + createProgressBar(type) { + const progress = document.createElement('div'); + progress.className = 'notification-progress'; + switch(type) { + case 'success': + progress.style.background = 'var(--success-color, #4caf50)'; + break; + case 'error': + progress.style.background = 'var(--error-color, #f44336)'; + break; + case 'info': + default: + progress.style.background = 'var(--info-color, #2196f3)'; + break; + } + return progress; + } +} + +// Initialize the notification system +const notifications = new NotificationSystem(); \ No newline at end of file diff --git a/js/onboarding.js b/js/onboarding.js new file mode 100644 index 0000000..d9315ea --- /dev/null +++ b/js/onboarding.js @@ -0,0 +1,73 @@ +// Onboarding module +const onboarding = { + // Check if onboarding is complete + isComplete: () => { + return Storage.get('onboardingComplete') === true; + }, + + // Start the onboarding process + start: () => { + const modal = document.getElementById('onboarding-modal'); + const mainContent = document.getElementById('main-content'); + + if (!onboarding.isComplete()) { + modal.classList.remove('hidden'); + modal.classList.add('active'); + mainContent.classList.add('hidden'); + + document.getElementById('next-step-btn').addEventListener('click', () => onboarding.nextStep(1)); + document.getElementById('complete-setup-btn').addEventListener('click', onboarding.complete); + + // Set up search engine selection + const engines = document.querySelectorAll('.search-engine-option'); + engines.forEach(engine => { + engine.addEventListener('click', () => { + engines.forEach(e => e.classList.remove('selected')); + engine.classList.add('selected'); + }); + }); + } else { + modal.classList.add('hidden'); + mainContent.classList.remove('hidden'); + } + }, + + // Move to the next step in onboarding + nextStep: (currentStep) => { + const currentStepEl = document.querySelector(`[data-step="${currentStep}"]`); + const nextStepEl = document.querySelector(`[data-step="${currentStep + 1}"]`); + const name = document.getElementById('user-name').value.trim(); + + if (!name) { + notifications.show('Please enter your name!', 'error'); + return; + } + + Storage.set('userName', name); + + currentStepEl.classList.add('hidden'); + nextStepEl.classList.remove('hidden'); + nextStepEl.classList.add('visible'); + }, + + // Complete the onboarding process + complete: () => { + const selectedEngine = document.querySelector('.search-engine-option.selected'); + if (!selectedEngine) { + notifications.show('Please select a search engine!', 'error'); + return; + } + + const searchEngine = selectedEngine.dataset.engine; + Storage.set('searchEngine', searchEngine); + Storage.set('onboardingComplete', true); + + const modal = document.getElementById('onboarding-modal'); + const mainContent = document.getElementById('main-content'); + + modal.classList.add('hidden'); + mainContent.classList.remove('hidden'); + notifications.show('Welcome to your new tab! 👋', 'success'); + updateGreeting(); + } +}; \ No newline at end of file diff --git a/js/search.js b/js/search.js new file mode 100644 index 0000000..7998479 --- /dev/null +++ b/js/search.js @@ -0,0 +1,47 @@ +const search = { + // Supported search engines and their URLs + engines: { + google: 'https://www.google.com/search?q=', + bing: 'https://www.bing.com/search?q=', + duckduckgo: 'https://duckduckgo.com/?q=', + brave: 'https://search.brave.com/search?q=', + qwant: 'https://www.qwant.com/?q=', + searxng: 'https://searx.org/search?q=' + }, + + // Perform search using selected engine + perform: () => { + const searchBar = document.getElementById('search-bar'); + const query = searchBar.value.trim(); + const engine = Storage.get('searchEngine') || 'google'; + + if (query) { + const searchUrl = search.engines[engine] + encodeURIComponent(query); + window.location.href = searchUrl; + } + }, + + // Initialize search functionality + init: () => { + const searchBar = document.getElementById('search-bar'); + const searchButton = document.getElementById('search-button'); + + searchBar.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + search.perform(); + } + }); + + searchButton.addEventListener('click', search.perform); + + // Global keyboard shortcut to focus search bar + document.addEventListener('keydown', (e) => { + if (e.key === '/' && + !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) && + window.getSelection().toString() === '') { + e.preventDefault(); + searchBar.focus(); + } + }); + } +}; \ No newline at end of file diff --git a/js/settings.js b/js/settings.js new file mode 100644 index 0000000..ea0ab68 --- /dev/null +++ b/js/settings.js @@ -0,0 +1,336 @@ +// Anonymous name generator +const anonymousNames = { + adjectives: ['Hidden', 'Secret', 'Mystery', 'Shadow', 'Unknown', 'Silent', 'Stealth', 'Phantom', 'Ghost', 'Anon'], + nouns: [' User', ' Visitor', ' Guest', ' Agent', ' Entity', ' Person', ' Browser', ' Explorer', ' Wanderer', ' Navigator'], + + generate: function() { + const adjective = this.adjectives[Math.floor(Math.random() * this.adjectives.length)]; + const noun = this.nouns[Math.floor(Math.random() * this.nouns.length)]; + return `${adjective}${noun}`; + } +}; + +// Main settings object +const settings = { + // Toggle between light and dark themes + toggleTheme: () => { + const currentTheme = document.body.getAttribute('data-theme'); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + document.body.setAttribute('data-theme', newTheme); + Storage.set('theme', newTheme); + + const themeIcon = document.querySelector('#toggle-theme i'); + themeIcon.className = `fas fa-${newTheme === 'dark' ? 'sun' : 'moon'}`; + }, + + // Toggle anonymous mode + toggleAnonymousMode: () => { + const isAnonymous = Storage.get('anonymousMode') || false; + Storage.set('anonymousMode', !isAnonymous); + + if (!isAnonymous) { + const randomName = anonymousNames.generate(); + Storage.set('anonymousName', randomName); + notifications.show('Anonymous mode enabled!', 'info'); + } else { + Storage.remove('anonymousName'); + notifications.show('Anonymous mode disabled!', 'info'); + } + + shortcuts.render(); + updateGreeting(); + }, + + // Update the search engine + updateSearchEngine: (engine) => { + Storage.set('searchEngine', engine); + notifications.show('Search engine updated successfully!', 'success'); + }, + + // Update visibility of UI elements + updateVisibility: () => { + const elements = { + greeting: { + id: 'greeting', + toggle: 'toggle-greeting', + functions: ['updateGreeting'], + name: 'Greeting' + }, + search: { + id: 'search-container', + toggle: 'toggle-search', + functions: ['search.init', 'search.perform'], + name: 'Search bar' + }, + shortcuts: { + id: 'shortcuts-grid', + toggle: 'toggle-shortcuts', + functions: ['shortcuts.init', 'shortcuts.render'], + name: 'Shortcuts' + }, + addShortcut: { + id: 'add-shortcut', + toggle: 'toggle-add-shortcut', + functions: [], + name: 'Add shortcut button' + } + }; + + Object.entries(elements).forEach(([key, element]) => { + const isVisible = Storage.get(`show_${key}`); + if (isVisible === null) Storage.set(`show_${key}`, true); + + const toggle = document.getElementById(element.toggle); + const elementNode = document.getElementById(element.id); + + if (toggle && elementNode) { + toggle.checked = isVisible !== false; + if (isVisible === false) { + elementNode.style.visibility = 'hidden'; + elementNode.style.opacity = '0'; + elementNode.style.position = 'absolute'; + elementNode.style.pointerEvents = 'none'; + } + + toggle.addEventListener('change', (e) => { + const isChecked = e.target.checked; + Storage.set(`show_${key}`, isChecked); + + if (isChecked) { + elementNode.style.visibility = 'visible'; + elementNode.style.opacity = '1'; + elementNode.style.position = 'relative'; + elementNode.style.pointerEvents = 'auto'; + } else { + elementNode.style.visibility = 'hidden'; + elementNode.style.opacity = '0'; + elementNode.style.position = 'absolute'; + elementNode.style.pointerEvents = 'none'; + } + + if (key === 'shortcuts') { + const addShortcutBtn = document.getElementById('add-shortcut'); + const addShortcutVisible = Storage.get('show_addShortcut') !== false; + + if (addShortcutBtn && !isChecked && !addShortcutVisible) { + addShortcutBtn.style.visibility = 'hidden'; + addShortcutBtn.style.opacity = '0'; + addShortcutBtn.style.position = 'absolute'; + addShortcutBtn.style.pointerEvents = 'none'; + } else if (addShortcutBtn && addShortcutVisible) { + addShortcutBtn.style.visibility = 'visible'; + addShortcutBtn.style.opacity = '1'; + addShortcutBtn.style.position = 'relative'; + addShortcutBtn.style.pointerEvents = 'auto'; + } + } + + notifications.show( + `${element.name} ${isChecked ? 'shown' : 'hidden'}!`, + isChecked ? 'success' : 'info' + ); + }); + } + }); + }, + + // Initialize settings + init: () => { + const settingsButton = document.getElementById('settings-button'); + const settingsModal = document.getElementById('settings-modal'); + const closeSettings = document.getElementById('close-settings'); + + settingsButton.addEventListener('click', (e) => { + e.stopPropagation(); + const userName = Storage.get('userName') || ''; + const isAnonymous = Storage.get('anonymousMode') || false; + const currentEngine = Storage.get('searchEngine') || 'google'; + + document.getElementById('settings-name').value = userName; + document.getElementById('toggle-anonymous').checked = isAnonymous; + document.getElementById('search-engine-select').value = currentEngine; + + settingsModal.classList.remove('hidden'); + settingsModal.classList.add('active'); + }); + + closeSettings.addEventListener('click', () => { + settingsModal.classList.remove('active'); + setTimeout(() => { + settingsModal.classList.add('hidden'); + }, 300); + }); + + const themeToggle = document.getElementById('toggle-theme'); + themeToggle.addEventListener('click', settings.toggleTheme); + + const anonymousToggle = document.getElementById('toggle-anonymous'); + anonymousToggle.addEventListener('change', settings.toggleAnonymousMode); + + const searchEngineSelect = document.getElementById('search-engine-select'); + searchEngineSelect.addEventListener('change', (e) => { + settings.updateSearchEngine(e.target.value); + }); + + const nameInput = document.getElementById('settings-name'); + nameInput.addEventListener('change', (e) => { + const newName = e.target.value.trim(); + if (newName) { + Storage.set('userName', newName); + updateGreeting(); + notifications.show('Name updated successfully!', 'success'); + } + }); + + const savedTheme = Storage.get('theme') || 'light'; + document.body.setAttribute('data-theme', savedTheme); + const themeIcon = document.querySelector('#toggle-theme i'); + themeIcon.className = `fas fa-${savedTheme === 'dark' ? 'sun' : 'moon'}`; + + settings.initDataManagement(); + settings.updateVisibility(); + } +}; + +// Initialize data management +settings.initDataManagement = () => { + const exportBtn = document.getElementById('export-data'); + const importBtn = document.getElementById('import-data'); + const resetBtn = document.getElementById('reset-data'); + const fileInput = document.getElementById('import-file'); + + // Export Data + exportBtn.addEventListener('click', () => { + try { + const data = { + settings: { + theme: Storage.get('theme'), + userName: Storage.get('userName'), + anonymousMode: Storage.get('anonymousMode'), + anonymousName: Storage.get('anonymousName'), + searchEngine: Storage.get('searchEngine'), + show_greeting: Storage.get('show_greeting'), + show_search: Storage.get('show_search'), + show_shortcuts: Storage.get('show_shortcuts'), + show_addShortcut: Storage.get('show_addShortcut') + }, + shortcuts: Storage.get('shortcuts') || [] + }; + + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'jstar-tab-backup.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + notifications.show('Data exported successfully!', 'success'); + } catch (error) { + notifications.show('Failed to export data!', 'error'); + console.error('Export error:', error); + } + }); + + // Import Data + importBtn.addEventListener('click', () => fileInput.click()); + + fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const data = JSON.parse(event.target.result); + + if (!data.shortcuts || !data.settings) { + throw new Error('Invalid backup file format'); + } + + Object.entries(data.settings).forEach(([key, value]) => { + Storage.set(key, value); + }); + + Storage.set('shortcuts', data.shortcuts); + + fileInput.value = ''; + + ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { + const isVisible = data.settings[`show_${element}`]; + const elementNode = document.getElementById(element === 'search' ? 'search-container' : element); + const toggle = document.getElementById(`toggle-${element}`); + + if (elementNode && toggle) { + toggle.checked = isVisible !== false; + if (isVisible === false) { + elementNode.style.visibility = 'hidden'; + elementNode.style.opacity = '0'; + elementNode.style.position = 'absolute'; + elementNode.style.pointerEvents = 'none'; + } else { + elementNode.style.visibility = 'visible'; + elementNode.style.opacity = '1'; + elementNode.style.position = 'relative'; + elementNode.style.pointerEvents = 'auto'; + } + } + }); + + const shortcutsVisible = data.settings.show_shortcuts !== false; + const addShortcutVisible = data.settings.show_addShortcut !== false; + const addShortcutBtn = document.getElementById('add-shortcut'); + + if (addShortcutBtn) { + if (!shortcutsVisible && !addShortcutVisible) { + addShortcutBtn.style.visibility = 'hidden'; + addShortcutBtn.style.opacity = '0'; + addShortcutBtn.style.position = 'absolute'; + addShortcutBtn.style.pointerEvents = 'none'; + } else if (addShortcutVisible) { + addShortcutBtn.style.visibility = 'visible'; + addShortcutBtn.style.opacity = '1'; + addShortcutBtn.style.position = 'relative'; + addShortcutBtn.style.pointerEvents = 'auto'; + } + } + + shortcuts.render(); + updateGreeting(); + document.body.setAttribute('data-theme', data.settings.theme || 'light'); + + notifications.show('Data imported successfully!', 'success'); + } catch (error) { + notifications.show('Failed to import data: Invalid file format!', 'error'); + console.error('Import error:', error); + } + }; + + reader.onerror = () => { + notifications.show('Failed to read file!', 'error'); + }; + + reader.readAsText(file); + }); + + // Reset Data + resetBtn.addEventListener('click', () => { + const confirmReset = confirm('Are you sure you want to reset all data? This action cannot be undone.'); + + if (confirmReset) { + try { + Storage.clear(); + closeModal(document.getElementById('settings-modal')); + notifications.show('All data has been reset!', 'success'); + setTimeout(() => { + window.location.reload(); + }, 1000); + } catch (error) { + notifications.show('Failed to reset data!', 'error'); + console.error('Reset error:', error); + } + } + }); +}; \ No newline at end of file diff --git a/js/shortcuts.js b/js/shortcuts.js new file mode 100644 index 0000000..909803f --- /dev/null +++ b/js/shortcuts.js @@ -0,0 +1,255 @@ +const shortcuts = { + MAX_SHORTCUTS: 12, + + // Validate and format URL + validateAndFormatUrl: (url) => { + if (!/^https?:\/\//i.test(url)) { + url = 'https://' + url; + } + + try { + new URL(url); + return url; + } catch (e) { + return false; + } + }, + + // Add new shortcut + add: (url, name) => { + const currentShortcuts = Storage.get('shortcuts') || []; + if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { + notifications.show('Maximum shortcuts limit reached!', 'error'); + return; + } + + const formattedUrl = shortcuts.validateAndFormatUrl(url); + if (!formattedUrl) { + notifications.show('Invalid URL format!', 'error'); + return; + } + + currentShortcuts.push({ url: formattedUrl, name }); + Storage.set('shortcuts', currentShortcuts); + shortcuts.render(); + }, + + // Remove shortcut + remove: (index) => { + const currentShortcuts = Storage.get('shortcuts') || []; + currentShortcuts.splice(index, 1); + Storage.set('shortcuts', currentShortcuts); + shortcuts.render(); + notifications.show('Shortcut removed!', 'success'); + }, + + // Edit existing shortcut + edit: (index, newUrl, newName) => { + const currentShortcuts = Storage.get('shortcuts') || []; + currentShortcuts[index] = { url: newUrl, name: newName }; + Storage.set('shortcuts', currentShortcuts); + shortcuts.render(); + notifications.show('Shortcut updated!', 'success'); + }, + + // Show context menu for shortcut + showContextMenu: (e, index) => { + e.preventDefault(); + const menu = document.getElementById('context-menu'); + const rect = e.target.getBoundingClientRect(); + + menu.style.top = `${e.clientY}px`; + menu.style.left = `${e.clientX}px`; + menu.classList.remove('hidden'); + menu.dataset.shortcutIndex = index; + + const handleClickOutside = (event) => { + if (!menu.contains(event.target)) { + menu.classList.add('hidden'); + document.removeEventListener('click', handleClickOutside); + } + }; + + setTimeout(() => { + document.addEventListener('click', handleClickOutside); + }, 0); + }, + + // Render shortcuts grid + render: () => { + const grid = document.getElementById('shortcuts-grid'); + const currentShortcuts = Storage.get('shortcuts') || []; + const isAnonymous = Storage.get('anonymousMode') || false; + + grid.innerHTML = ''; + + currentShortcuts.forEach((shortcut, index) => { + const element = document.createElement('div'); + element.className = `shortcut ${isAnonymous ? 'blurred' : ''}`; + + const icon = document.createElement('img'); + icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`; + icon.alt = shortcut.name; + + const name = document.createElement('span'); + name.textContent = shortcut.name; + + element.appendChild(icon); + element.appendChild(name); + + element.addEventListener('click', (e) => { + if (e.ctrlKey) { + window.open(shortcut.url, '_blank'); + } else { + window.location.href = shortcut.url; + } + }); + + element.addEventListener('contextmenu', (e) => { + e.preventDefault(); + const menu = document.getElementById('context-menu'); + + menu.style.top = `${e.pageY}px`; + menu.style.left = `${e.pageX}px`; + menu.classList.remove('hidden'); + menu.dataset.shortcutIndex = index; + + const closeMenu = (event) => { + if (!menu.contains(event.target)) { + menu.classList.add('hidden'); + document.removeEventListener('click', closeMenu); + } + }; + + setTimeout(() => { + document.addEventListener('click', closeMenu); + }, 0); + }); + + grid.appendChild(element); + }); + }, + + // Initialize shortcuts functionality + init: () => { + const addShortcutButton = document.getElementById('add-shortcut'); + if (addShortcutButton) { + addShortcutButton.addEventListener('click', (e) => { + e.stopPropagation(); + const currentShortcuts = Storage.get('shortcuts') || []; + + if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { + notifications.show('Maximum shortcuts limit reached!', 'error'); + return; + } + + const modal = document.getElementById('add-shortcut-modal'); + if (modal) { + modal.classList.remove('hidden'); + modal.classList.add('active'); + + const urlInput = document.getElementById('shortcut-url'); + const nameInput = document.getElementById('shortcut-name'); + + const saveShortcutButton = document.getElementById('save-shortcut'); + if (saveShortcutButton) { + saveShortcutButton.onclick = () => { + const url = urlInput.value.trim(); + const name = nameInput.value.trim(); + + if (url && name) { + try { + new URL(url); + shortcuts.add(url, name); + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + urlInput.value = ''; + nameInput.value = ''; + }, 300); + notifications.show('Shortcut added successfully!', 'success'); + } catch (e) { + notifications.show('Invalid URL format!', 'error'); + } + } + }; + } + + const cancelShortcutButton = document.getElementById('cancel-shortcut'); + if (cancelShortcutButton) { + cancelShortcutButton.onclick = () => { + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + urlInput.value = ''; + nameInput.value = ''; + }, 300); + }; + } + } + }); + } + + // Context menu actions + const contextMenu = document.getElementById('context-menu'); + if (contextMenu) { + contextMenu.addEventListener('click', (e) => { + const action = e.target.closest('.context-menu-item')?.dataset.action; + const index = parseInt(contextMenu.dataset.shortcutIndex); + + if (action === 'edit') { + const currentShortcuts = Storage.get('shortcuts') || []; + const shortcut = currentShortcuts[index]; + const modal = document.getElementById('edit-shortcut-modal'); + + if (modal) { + const urlInput = document.getElementById('edit-shortcut-url'); + const nameInput = document.getElementById('edit-shortcut-name'); + + urlInput.value = shortcut.url; + nameInput.value = shortcut.name; + + modal.classList.remove('hidden'); + modal.classList.add('active'); + + const saveButton = document.getElementById('save-edit-shortcut'); + const closeButton = document.getElementById('close-edit-shortcut'); + const cancelButton = document.getElementById('cancel-edit-shortcut'); + + const closeModal = () => { + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); + }; + + const handleSave = () => { + const newUrl = urlInput.value.trim(); + const newName = nameInput.value.trim(); + + if (newUrl && newName) { + const formattedUrl = shortcuts.validateAndFormatUrl(newUrl); + if (formattedUrl) { + shortcuts.edit(index, formattedUrl, newName); + closeModal(); + } else { + notifications.show('Invalid URL format!', 'error'); + } + } + }; + + saveButton.onclick = handleSave; + closeButton.onclick = closeModal; + cancelButton.onclick = closeModal; + } + } else if (action === 'delete') { + shortcuts.remove(index); + } + + contextMenu.classList.add('hidden'); + }); + } + + shortcuts.render(); + } +}; \ No newline at end of file diff --git a/js/storage.js b/js/storage.js new file mode 100644 index 0000000..2f451e8 --- /dev/null +++ b/js/storage.js @@ -0,0 +1,44 @@ +/** + * Storage utility object for managing localStorage operations + * All methods handle JSON parsing/stringifying and error cases + */ +const Storage = { + // Retrieve and parse stored value + get: (key) => { + try { + return JSON.parse(localStorage.getItem(key)); + } catch (e) { + return null; + } + }, + + // Store value as JSON string + set: (key, value) => { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (e) { + return false; + } + }, + + // Delete specific key + remove: (key) => { + try { + localStorage.removeItem(key); + return true; + } catch (e) { + return false; + } + }, + + // Remove all stored data + clear: () => { + try { + localStorage.clear(); + return true; + } catch (e) { + return false; + } + } +}; \ No newline at end of file