mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-04-25 14:08:54 -05:00
f5c3c3f59f
Fixed multiple issues with keyboard shortcuts and browser notifications: Keyboard Shortcuts: - Fixed Ctrl+/ not working to focus search input - Resolved conflict between three event handlers (base.html, commands.js, keyboard-shortcuts-advanced.js) - Changed inline handler from Ctrl+K to Ctrl+/ to avoid command palette conflict - Updated search bar UI badge to display Ctrl+/ instead of Ctrl+K - Removed conflicting ? key handler from commands.js (now uses Shift+? for shortcuts panel) - Improved key detection to properly handle special characters like / and ? - Added debug logging for troubleshooting keyboard events Final keyboard mapping: - Ctrl+K: Open Command Palette - Ctrl+/: Focus Search Input - Shift+?: Show All Keyboard Shortcuts - Esc: Close Modals/Panels Notification System: - Fixed "right-hand side of 'in' should be an object" error in smart-notifications.js - Changed notification permission request to follow browser security policies - Permission now checked silently on load, only requested on user interaction - Added "Enable Notifications" banner in notification center panel - Fixed service worker sync check to properly verify registration object Browser Compatibility: - All fixes respect browser security policies for notification permissions - Graceful degradation when service worker features unavailable - Works correctly on Chrome, Firefox, Safari, and Edge Files modified: - app/static/enhanced-search.js - app/static/keyboard-shortcuts-advanced.js - app/static/smart-notifications.js - app/templates/base.html - app/static/commands.js Closes issues with keyboard shortcuts not responding and browser console errors.
254 lines
7.9 KiB
JavaScript
254 lines
7.9 KiB
JavaScript
/**
|
|
* Quick Actions Floating Menu
|
|
* Floating action button with quick access to common actions
|
|
*/
|
|
|
|
class QuickActionsMenu {
|
|
constructor() {
|
|
this.isOpen = false;
|
|
this.button = null;
|
|
this.menu = null;
|
|
this.actions = this.defineActions();
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.createButton();
|
|
this.createMenu();
|
|
this.attachEventListeners();
|
|
|
|
// Show/hide based on scroll
|
|
this.handleScroll();
|
|
window.addEventListener('scroll', () => this.handleScroll());
|
|
}
|
|
|
|
defineActions() {
|
|
return [
|
|
{
|
|
id: 'start-timer',
|
|
icon: 'fas fa-play',
|
|
label: 'Start Timer',
|
|
color: 'bg-green-500 hover:bg-green-600',
|
|
action: () => this.startTimer(),
|
|
shortcut: 't s'
|
|
},
|
|
{
|
|
id: 'log-time',
|
|
icon: 'fas fa-clock',
|
|
label: 'Log Time',
|
|
color: 'bg-blue-500 hover:bg-blue-600',
|
|
action: () => window.location.href = '/timer/manual_entry',
|
|
shortcut: 't l'
|
|
},
|
|
{
|
|
id: 'new-project',
|
|
icon: 'fas fa-folder-plus',
|
|
label: 'New Project',
|
|
color: 'bg-purple-500 hover:bg-purple-600',
|
|
action: () => window.location.href = '/projects/create',
|
|
shortcut: 'c p'
|
|
},
|
|
{
|
|
id: 'new-task',
|
|
icon: 'fas fa-tasks',
|
|
label: 'New Task',
|
|
color: 'bg-orange-500 hover:bg-orange-600',
|
|
action: () => window.location.href = '/tasks/create',
|
|
shortcut: 'c t'
|
|
},
|
|
{
|
|
id: 'new-client',
|
|
icon: 'fas fa-user-plus',
|
|
label: 'New Client',
|
|
color: 'bg-indigo-500 hover:bg-indigo-600',
|
|
action: () => window.location.href = '/clients/create',
|
|
shortcut: 'c c'
|
|
},
|
|
{
|
|
id: 'quick-report',
|
|
icon: 'fas fa-chart-line',
|
|
label: 'Quick Report',
|
|
color: 'bg-pink-500 hover:bg-pink-600',
|
|
action: () => window.location.href = '/reports/',
|
|
shortcut: 'g r'
|
|
}
|
|
];
|
|
}
|
|
|
|
createButton() {
|
|
this.button = document.createElement('button');
|
|
this.button.id = 'quickActionsButton';
|
|
this.button.className = 'fixed bottom-6 right-6 z-40 w-14 h-14 bg-primary text-white rounded-full shadow-lg hover:shadow-xl hover:scale-110 transition-all duration-200 flex items-center justify-center group';
|
|
this.button.setAttribute('aria-label', 'Quick actions');
|
|
this.button.innerHTML = `
|
|
<i class="fas fa-bolt text-xl transition-transform duration-200 group-hover:rotate-12"></i>
|
|
`;
|
|
document.body.appendChild(this.button);
|
|
}
|
|
|
|
createMenu() {
|
|
this.menu = document.createElement('div');
|
|
this.menu.id = 'quickActionsMenu';
|
|
this.menu.className = 'fixed bottom-24 right-6 z-40 hidden';
|
|
|
|
let menuHTML = '<div class="flex flex-col gap-2">';
|
|
|
|
this.actions.forEach((action, index) => {
|
|
menuHTML += `
|
|
<button
|
|
data-action="${action.id}"
|
|
class="${action.color} text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 transition-all duration-200 hover:scale-105 hover:shadow-xl min-w-[200px] group"
|
|
style="animation: slideInRight 0.3s ease-out ${index * 0.05}s both;"
|
|
title="${action.shortcut ? 'Shortcut: ' + action.shortcut : ''}"
|
|
>
|
|
<i class="${action.icon} text-lg group-hover:scale-110 transition-transform"></i>
|
|
<span class="font-medium flex-1 text-left">${action.label}</span>
|
|
${action.shortcut ? `<kbd class="text-xs opacity-75 bg-white/20 px-2 py-1 rounded">${action.shortcut}</kbd>` : ''}
|
|
</button>
|
|
`;
|
|
});
|
|
|
|
menuHTML += '</div>';
|
|
this.menu.innerHTML = menuHTML;
|
|
document.body.appendChild(this.menu);
|
|
|
|
// Add CSS animation
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@keyframes slideInRight {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
#quickActionsButton.open i {
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
#quickActionsMenu {
|
|
right: 1rem;
|
|
bottom: 5.5rem;
|
|
}
|
|
#quickActionsMenu button {
|
|
min-width: calc(100vw - 2rem);
|
|
}
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
attachEventListeners() {
|
|
// Toggle menu
|
|
this.button.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.toggle();
|
|
});
|
|
|
|
// Action buttons
|
|
this.menu.querySelectorAll('[data-action]').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const actionId = e.currentTarget.dataset.action;
|
|
const action = this.actions.find(a => a.id === actionId);
|
|
if (action) {
|
|
action.action();
|
|
this.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Close on outside click
|
|
document.addEventListener('click', (e) => {
|
|
if (this.isOpen && !this.menu.contains(e.target) && e.target !== this.button) {
|
|
this.close();
|
|
}
|
|
});
|
|
|
|
// Close on escape
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && this.isOpen) {
|
|
this.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
toggle() {
|
|
if (this.isOpen) {
|
|
this.close();
|
|
} else {
|
|
this.open();
|
|
}
|
|
}
|
|
|
|
open() {
|
|
this.isOpen = true;
|
|
this.menu.classList.remove('hidden');
|
|
this.button.classList.add('open');
|
|
|
|
// Animate button
|
|
this.button.style.transform = 'rotate(45deg)';
|
|
}
|
|
|
|
close() {
|
|
this.isOpen = false;
|
|
this.menu.classList.add('hidden');
|
|
this.button.classList.remove('open');
|
|
|
|
// Reset button
|
|
this.button.style.transform = 'rotate(0deg)';
|
|
}
|
|
|
|
handleScroll() {
|
|
const scrollY = window.scrollY;
|
|
|
|
// Hide when scrolling down, show when scrolling up
|
|
if (scrollY > this.lastScrollY && scrollY > 200) {
|
|
this.button.style.transform = 'translateY(100px)';
|
|
} else {
|
|
this.button.style.transform = this.isOpen ? 'rotate(45deg)' : 'translateY(0)';
|
|
}
|
|
|
|
this.lastScrollY = scrollY;
|
|
}
|
|
|
|
startTimer() {
|
|
// Try to find and click start timer button
|
|
const startBtn = document.querySelector('#openStartTimer, button[onclick*="startTimer"]');
|
|
if (startBtn) {
|
|
startBtn.click();
|
|
} else {
|
|
window.location.href = '/timer/manual_entry';
|
|
}
|
|
}
|
|
|
|
// Add custom action
|
|
addAction(action) {
|
|
this.actions.push(action);
|
|
this.recreateMenu();
|
|
}
|
|
|
|
// Remove action
|
|
removeAction(actionId) {
|
|
this.actions = this.actions.filter(a => a.id !== actionId);
|
|
this.recreateMenu();
|
|
}
|
|
|
|
recreateMenu() {
|
|
this.menu.remove();
|
|
this.createMenu();
|
|
this.attachEventListeners();
|
|
}
|
|
}
|
|
|
|
// Initialize
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
window.quickActionsMenu = new QuickActionsMenu();
|
|
console.log('Quick Actions menu initialized');
|
|
});
|
|
|