Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
519 changes: 302 additions & 217 deletions Frontend/src/App.jsx

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions Frontend/src/admin/components/ShortcutsHelpModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { useEffect } from 'react';
import { X, Keyboard } from 'lucide-react';
import { Card, CardHeader, CardTitle, CardContent } from '../../components/ui/card';

const shortcuts = [
{ keys: 'G + D', label: 'Go to Dashboard' },
{ keys: 'G + T', label: 'Go to Tickets' },
{ keys: 'G + S', label: 'Go to Settings' },
{ keys: 'G + P', label: 'Go to Profile' },
{ keys: 'G + H', label: 'Go to Help' },
{ keys: 'G + U', label: 'Go to Users (admin)' },
{ keys: 'G + A', label: 'Go to Analytics (admin)' },
{ keys: 'Ctrl + F', label: 'Focus search bar' },
{ keys: '?', label: 'Toggle this help' },
{ keys: 'Esc', label: 'Close this help' },
];

const ShortcutsHelpModal = ({ isOpen, onClose, isAdmin = false }) => {
useEffect(() => {
if (!isOpen) return;
const handler = (e) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [isOpen, onClose]);

if (!isOpen) return null;

const visibleShortcuts = shortcuts.filter((s) => {
if (!isAdmin && (s.label.includes('(admin)'))) return false;
return true;
});

return (
<div className="fixed inset-0 z-[100] flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
onClick={onClose}
/>

{/* Modal */}
<div className="relative w-full max-w-lg mx-4 animate-in fade-in zoom-in-95 duration-200">
<Card className="shadow-2xl border border-gray-200">
<CardHeader className="flex flex-row items-center justify-between border-b border-gray-100 pb-4">
<div className="flex items-center gap-2">
<Keyboard className="w-5 h-5 text-emerald-600" />
<CardTitle className="text-lg font-bold text-gray-900">Keyboard Shortcuts</CardTitle>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Close shortcuts help"
data-modal-close
>
<X className="w-4 h-4" />
</button>
</CardHeader>

<CardContent className="pt-4 pb-2">
<div className="space-y-1.5">
{visibleShortcuts.map((shortcut) => (
<div
key={shortcut.keys}
className="flex items-center justify-between py-2 px-1 rounded-lg hover:bg-gray-50 transition-colors"
>
<span className="text-sm text-gray-700">{shortcut.label}</span>
<kbd className="inline-flex items-center gap-0.5 px-2 py-1 text-xs font-mono font-medium text-gray-600 bg-gray-100 border border-gray-200 rounded-md">
{shortcut.keys.split(' + ').map((part, i) => (
<React.Fragment key={i}>
{i > 0 && <span className="text-gray-400 mx-0.5">+</span>}
<span className="px-1 py-0.5 bg-white rounded border border-gray-200 shadow-sm">
{part}
</span>
</React.Fragment>
))}
</kbd>
</div>
))}
</div>

<p className="text-xs text-gray-400 mt-4 pb-1 text-center">
Press <kbd className="px-1 py-0.5 text-[10px] font-mono bg-gray-100 border border-gray-200 rounded">?</kbd> anytime to toggle this help
</p>
</CardContent>
</Card>
</div>
</div>
);
};

export default ShortcutsHelpModal;
98 changes: 54 additions & 44 deletions Frontend/src/admin/layout/AdminLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,73 @@ import { Outlet } from 'react-router-dom';
import AdminSidebar from '../components/AdminSidebar';
import AdminHeader from '../components/AdminHeader';
import NotificationToast from '../../user/components/NotificationToast';
import useKeyboardShortcuts from '../../hooks/useKeyboardShortcuts';
import ShortcutsHelpModal from '../../admin/components/ShortcutsHelpModal';

/**
* AdminLayout Component
* Master framework for the administrative zone.
* Enforces a fixed-sidebar architecture with a centered, high-density content terminal.
* Integrates global keyboard shortcuts (Issue #1172).
*/
const AdminLayout = () => {
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const { showHelp, setShowHelp } = useKeyboardShortcuts({}, { role: 'admin' });

return (
<div className="flex h-screen bg-[#f8faf9] overflow-hidden font-sans">
{/* Master Navigation Column (Responsive) */}
<div
className={`hidden md:block flex-shrink-0 relative z-40 transition-all duration-300`}
style={{ width: isSidebarCollapsed ? '80px' : '260px' }}
>
<AdminSidebar isCollapsed={isSidebarCollapsed} onToggleCollapse={() => setIsSidebarCollapsed(!isSidebarCollapsed)} />
</div>
return (
<div className="flex h-screen bg-[#f8faf9] overflow-hidden font-sans">
{/* Master Navigation Column (Responsive) */}
<div
className="hidden md:block flex-shrink-0 relative z-40 transition-all duration-300"
style={{ width: isSidebarCollapsed ? '80px' : '260px' }}
>
<AdminSidebar
isCollapsed={isSidebarCollapsed}
onToggleCollapse={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
/>
</div>

{/* Viewport Execution Layer */}
<div className="flex-1 flex flex-col min-w-0 relative h-full">
{/* Global Command Header */}
<AdminHeader
onMobileNavToggle={() => setIsMobileNavOpen(!isMobileNavOpen)}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebar={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
/>
{/* Viewport Execution Layer */}
<div className="flex-1 flex flex-col min-w-0 relative h-full">
{/* Global Command Header */}
<AdminHeader
onMobileNavToggle={() => setIsMobileNavOpen(!isMobileNavOpen)}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebar={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
/>

{/* Operational Workspace */}
<main className="flex-1 overflow-x-hidden overflow-y-auto custom-scrollbar relative">
{/* Centered Payload Container */}
<div className="max-w-[1280px] w-full mx-auto px-6 md:px-10 py-8 md:py-12 animate-in fade-in slide-in-from-bottom-6 duration-1000">
<Outlet />
</div>
</main>
</div>
{/* Operational Workspace */}
<main id="admin-main-content" className="flex-1 overflow-x-hidden overflow-y-auto custom-scrollbar relative">
{/* Centered Payload Container */}
<div className="max-w-[1280px] w-full mx-auto px-6 md:px-10 py-8 md:py-12 animate-in fade-in slide-in-from-bottom-6 duration-1000">
<Outlet />
</div>
</main>
</div>

{/* Real-time System Notifications */}
<NotificationToast />
{/* Real-time System Notifications */}
<NotificationToast />

{/* Mobile Nav Overlay (Emergency protocols) */}
{isMobileNavOpen && (
<div
className="fixed inset-0 bg-slate-900/80 backdrop-blur-md z-50 lg:hidden flex transition-opacity duration-300"
onClick={() => setIsMobileNavOpen(false)}
>
<div
className="w-[85%] max-w-[280px] h-full shadow-2xl animate-in slide-in-from-left duration-300"
onClick={(e) => e.stopPropagation()}
>
<AdminSidebar isMobile={true} onClose={() => setIsMobileNavOpen(false)} />
</div>
</div>
)}
{/* Keyboard Shortcuts Help Modal */}
<ShortcutsHelpModal isOpen={showHelp} onClose={() => setShowHelp(false)} isAdmin={true} />

{/* Mobile Nav Overlay (Emergency protocols) */}
{isMobileNavOpen && (
<div
className="fixed inset-0 bg-slate-900/80 backdrop-blur-md z-50 lg:hidden flex transition-opacity duration-300"
onClick={() => setIsMobileNavOpen(false)}
>
<div
className="w-[85%] max-w-[280px] h-full shadow-2xl animate-in slide-in-from-left duration-300"
onClick={(e) => e.stopPropagation()}
>
<AdminSidebar isMobile={true} onClose={() => setIsMobileNavOpen(false)} />
</div>
</div>
);
)}
</div>
);
};

export default AdminLayout;
181 changes: 181 additions & 0 deletions Frontend/src/components/shared/ShortcutsHelp.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* Shortcuts Help Modal
* Displays available keyboard shortcuts in a styled overlay.
*/

import React, { useState, useEffect } from 'react';
import { formatShortcut, getShortcutDescription } from '../../hooks/useKeyboardShortcuts';

const ShortcutsHelp = ({ isOpen, onClose, shortcuts = {} }) => {
const [selectedCategory, setSelectedCategory] = useState('navigation');

useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);

useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}, [isOpen, onClose]);

if (!isOpen) return null;

// Categorize shortcuts
const categories = {
navigation: {
title: 'Navigation',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
),
shortcuts: ['g,d', 'g,t', 'g,n', 'g,p', 'g,h', 'g,u', 'g,s', 'g,a'],
},
actions: {
title: 'Quick Actions',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
),
shortcuts: ['ctrl+f', 'ctrl+k', 'ctrl+/', '?', 'escape'],
},
};

// Filter shortcuts based on what's available
const getAvailableShortcuts = (categoryShortcuts) => {
return categoryShortcuts.filter(s => s in shortcuts);
};

return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" data-modal>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>

{/* Modal */}
<div className="relative bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</div>
<div>
<h2 className="text-xl font-bold text-white">Keyboard Shortcuts</h2>
<p className="text-white/80 text-sm">Navigate faster with keyboard shortcuts</p>
</div>
</div>
<button
onClick={onClose}
className="text-white/80 hover:text-white transition-colors"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>

{/* Content */}
<div className="p-6 overflow-y-auto max-h-[60vh]">
{/* Category Tabs */}
<div className="flex space-x-2 mb-6">
{Object.entries(categories).map(([key, category]) => {
const available = getAvailableShortcuts(category.shortcuts);
if (available.length === 0) return null;

return (
<button
key={key}
onClick={() => setSelectedCategory(key)}
className={`flex items-center space-x-2 px-4 py-2 rounded-lg transition-colors ${
selectedCategory === key
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{category.icon}
<span className="font-medium">{category.title}</span>
</button>
);
})}
</div>

{/* Shortcuts List */}
<div className="space-y-3">
{categories[selectedCategory]?.shortcuts.map(shortcut => {
if (!(shortcut in shortcuts)) return null;

const description = getShortcutDescription(shortcut);
const formatted = formatShortcut(shortcut);

return (
<div
key={shortcut}
className="flex items-center justify-between p-4 bg-gray-50 rounded-xl hover:bg-gray-100 transition-colors"
>
<span className="text-gray-700 font-medium">{description}</span>
<div className="flex items-center space-x-1">
{formatted.split('').map((char, index) => (
<kbd
key={index}
className="px-2 py-1 bg-white border border-gray-300 rounded-md text-sm font-mono text-gray-600 shadow-sm"
>
{char}
</kbd>
))}
</div>
</div>
);
})}
</div>

{/* Tips */}
<div className="mt-6 p-4 bg-blue-50 rounded-xl">
<h3 className="text-sm font-semibold text-blue-800 mb-2">💡 Tips</h3>
<ul className="text-sm text-blue-700 space-y-1">
<li>• Press <kbd className="px-1 py-0.5 bg-blue-100 rounded text-xs">G</kbd> then wait for a second, then press the next key</li>
<li>• Shortcuts don't work when typing in input fields</li>
<li>• Press <kbd className="px-1 py-0.5 bg-blue-100 rounded text-xs">Esc</kbd> to close any modal</li>
</ul>
</div>
</div>

{/* Footer */}
<div className="border-t border-gray-200 p-4 bg-gray-50">
<div className="flex items-center justify-between">
<p className="text-sm text-gray-500">
Press <kbd className="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-mono">Ctrl</kbd> + <kbd className="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-mono">/</kbd> to toggle this help
</p>
<button
onClick={onClose}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
Got it!
</button>
</div>
</div>
</div>
</div>
);
};

export default ShortcutsHelp;
Loading