mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
Sidebar items can now be reordered; Settinsg for default view added
This commit is contained in:
@@ -1,29 +1,45 @@
|
||||
import { X, Keyboard } from 'lucide-react';
|
||||
import { Card, CardContent } from './Card';
|
||||
|
||||
interface KeyboardShortcutsModalProps {
|
||||
onClose: () => void;
|
||||
interface NavItem {
|
||||
id: string;
|
||||
to: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const shortcuts = [
|
||||
{ category: 'Navigation', items: [
|
||||
{ keys: ['1'], description: 'Go to Printers' },
|
||||
{ keys: ['2'], description: 'Go to Archives' },
|
||||
{ keys: ['3'], description: 'Go to Queue' },
|
||||
{ keys: ['4'], description: 'Go to Statistics' },
|
||||
{ keys: ['5'], description: 'Go to Cloud Profiles' },
|
||||
{ keys: ['6'], description: 'Go to Settings' },
|
||||
]},
|
||||
{ category: 'Archives', items: [
|
||||
{ keys: ['/'], description: 'Focus search' },
|
||||
{ keys: ['U'], description: 'Open upload modal' },
|
||||
{ keys: ['Esc'], description: 'Clear selection / blur input' },
|
||||
{ keys: ['Right-click'], description: 'Context menu on cards' },
|
||||
]},
|
||||
{ category: 'General', items: [
|
||||
{ keys: ['?'], description: 'Show this help' },
|
||||
]},
|
||||
];
|
||||
interface KeyboardShortcutsModalProps {
|
||||
onClose: () => void;
|
||||
navItems?: NavItem[];
|
||||
}
|
||||
|
||||
function getShortcuts(navItems?: NavItem[]) {
|
||||
const navShortcuts = navItems
|
||||
? navItems.map((item, index) => ({
|
||||
keys: [String(index + 1)],
|
||||
description: `Go to ${item.label}`,
|
||||
}))
|
||||
: [
|
||||
{ keys: ['1'], description: 'Go to Printers' },
|
||||
{ keys: ['2'], description: 'Go to Archives' },
|
||||
{ keys: ['3'], description: 'Go to Queue' },
|
||||
{ keys: ['4'], description: 'Go to Statistics' },
|
||||
{ keys: ['5'], description: 'Go to Cloud Profiles' },
|
||||
{ keys: ['6'], description: 'Go to Settings' },
|
||||
];
|
||||
|
||||
return [
|
||||
{ category: 'Navigation', items: navShortcuts },
|
||||
{ category: 'Archives', items: [
|
||||
{ keys: ['/'], description: 'Focus search' },
|
||||
{ keys: ['U'], description: 'Open upload modal' },
|
||||
{ keys: ['Esc'], description: 'Clear selection / blur input' },
|
||||
{ keys: ['Right-click'], description: 'Context menu on cards' },
|
||||
]},
|
||||
{ category: 'General', items: [
|
||||
{ keys: ['?'], description: 'Show this help' },
|
||||
]},
|
||||
];
|
||||
}
|
||||
|
||||
function KeyBadge({ children }: { children: string }) {
|
||||
return (
|
||||
@@ -33,7 +49,9 @@ function KeyBadge({ children }: { children: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function KeyboardShortcutsModal({ onClose }: KeyboardShortcutsModalProps) {
|
||||
export function KeyboardShortcutsModal({ onClose, navItems }: KeyboardShortcutsModalProps) {
|
||||
const shortcuts = getShortcuts(navItems);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onClose}>
|
||||
<Card className="w-full max-w-md" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
@@ -1,31 +1,136 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
|
||||
import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github } from 'lucide-react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, type LucideIcon } from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
|
||||
|
||||
const navItems = [
|
||||
{ to: '/', icon: Printer, label: 'Printers' },
|
||||
{ to: '/archives', icon: Archive, label: 'Archives' },
|
||||
{ to: '/queue', icon: Calendar, label: 'Queue' },
|
||||
{ to: '/stats', icon: BarChart3, label: 'Statistics' },
|
||||
{ to: '/cloud', icon: Cloud, label: 'Cloud Profiles' },
|
||||
{ to: '/settings', icon: Settings, label: 'Settings' },
|
||||
interface NavItem {
|
||||
id: string;
|
||||
to: string;
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const defaultNavItems: NavItem[] = [
|
||||
{ id: 'printers', to: '/', icon: Printer, label: 'Printers' },
|
||||
{ id: 'archives', to: '/archives', icon: Archive, label: 'Archives' },
|
||||
{ id: 'queue', to: '/queue', icon: Calendar, label: 'Queue' },
|
||||
{ id: 'stats', to: '/stats', icon: BarChart3, label: 'Statistics' },
|
||||
{ id: 'cloud', to: '/cloud', icon: Cloud, label: 'Cloud Profiles' },
|
||||
{ id: 'settings', to: '/settings', icon: Settings, label: 'Settings' },
|
||||
];
|
||||
|
||||
// Get ordered nav items from localStorage
|
||||
function getOrderedNavItems(): NavItem[] {
|
||||
const stored = localStorage.getItem('sidebarOrder');
|
||||
if (stored) {
|
||||
try {
|
||||
const order: string[] = JSON.parse(stored);
|
||||
const itemMap = new Map(defaultNavItems.map(item => [item.id, item]));
|
||||
const ordered: NavItem[] = [];
|
||||
for (const id of order) {
|
||||
const item = itemMap.get(id);
|
||||
if (item) {
|
||||
ordered.push(item);
|
||||
itemMap.delete(id);
|
||||
}
|
||||
}
|
||||
// Add any new items that weren't in the stored order
|
||||
for (const item of itemMap.values()) {
|
||||
ordered.push(item);
|
||||
}
|
||||
return ordered;
|
||||
} catch {
|
||||
return defaultNavItems;
|
||||
}
|
||||
}
|
||||
return defaultNavItems;
|
||||
}
|
||||
|
||||
// Save nav item order to localStorage
|
||||
function saveNavOrder(items: NavItem[]) {
|
||||
localStorage.setItem('sidebarOrder', JSON.stringify(items.map(i => i.id)));
|
||||
}
|
||||
|
||||
// Get default view from localStorage
|
||||
export function getDefaultView(): string {
|
||||
return localStorage.getItem('defaultView') || '/';
|
||||
}
|
||||
|
||||
// Save default view to localStorage
|
||||
export function setDefaultView(path: string) {
|
||||
localStorage.setItem('defaultView', path);
|
||||
}
|
||||
|
||||
export function Layout() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const [sidebarExpanded, setSidebarExpanded] = useState(() => {
|
||||
const stored = localStorage.getItem('sidebarExpanded');
|
||||
return stored !== 'false';
|
||||
});
|
||||
const [showShortcuts, setShowShortcuts] = useState(false);
|
||||
const [navItems, setNavItems] = useState<NavItem[]>(getOrderedNavItems);
|
||||
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
const hasRedirected = useRef(false);
|
||||
|
||||
// Redirect to default view on initial load
|
||||
useEffect(() => {
|
||||
if (!hasRedirected.current && location.pathname === '/') {
|
||||
const defaultView = getDefaultView();
|
||||
if (defaultView !== '/') {
|
||||
hasRedirected.current = true;
|
||||
navigate(defaultView, { replace: true });
|
||||
}
|
||||
}
|
||||
}, [location.pathname, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('sidebarExpanded', String(sidebarExpanded));
|
||||
}, [sidebarExpanded]);
|
||||
|
||||
// Drag and drop handlers
|
||||
const handleDragStart = (e: React.DragEvent, index: number) => {
|
||||
setDraggedIndex(index);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', String(index));
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, index: number) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
setDragOverIndex(index);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragOverIndex(null);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
|
||||
e.preventDefault();
|
||||
if (draggedIndex === null || draggedIndex === dropIndex) {
|
||||
setDraggedIndex(null);
|
||||
setDragOverIndex(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const newItems = [...navItems];
|
||||
const [draggedItem] = newItems.splice(draggedIndex, 1);
|
||||
newItems.splice(dropIndex, 0, draggedItem);
|
||||
|
||||
setNavItems(newItems);
|
||||
saveNavOrder(newItems);
|
||||
setDraggedIndex(null);
|
||||
setDragOverIndex(null);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setDraggedIndex(null);
|
||||
setDragOverIndex(null);
|
||||
};
|
||||
|
||||
// Global keyboard shortcuts for navigation
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
@@ -34,33 +139,16 @@ export function Layout() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Number keys for navigation (1-4)
|
||||
// Number keys for navigation (1-6) - follows sidebar order
|
||||
if (!e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
const keyNum = parseInt(e.key);
|
||||
if (keyNum >= 1 && keyNum <= navItems.length) {
|
||||
e.preventDefault();
|
||||
navigate(navItems[keyNum - 1].to);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case '1':
|
||||
e.preventDefault();
|
||||
navigate('/');
|
||||
break;
|
||||
case '2':
|
||||
e.preventDefault();
|
||||
navigate('/archives');
|
||||
break;
|
||||
case '3':
|
||||
e.preventDefault();
|
||||
navigate('/queue');
|
||||
break;
|
||||
case '4':
|
||||
e.preventDefault();
|
||||
navigate('/stats');
|
||||
break;
|
||||
case '5':
|
||||
e.preventDefault();
|
||||
navigate('/cloud');
|
||||
break;
|
||||
case '6':
|
||||
e.preventDefault();
|
||||
navigate('/settings');
|
||||
break;
|
||||
case '?':
|
||||
e.preventDefault();
|
||||
setShowShortcuts(true);
|
||||
@@ -70,7 +158,7 @@ export function Layout() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [navigate]);
|
||||
}, [navigate, navItems]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
@@ -95,12 +183,27 @@ export function Layout() {
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-2">
|
||||
<ul className="space-y-2">
|
||||
{navItems.map(({ to, icon: Icon, label }) => (
|
||||
<li key={to}>
|
||||
{navItems.map(({ id, to, icon: Icon, label }, index) => (
|
||||
<li
|
||||
key={id}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, index)}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => handleDrop(e, index)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className={`relative ${
|
||||
draggedIndex === index ? 'opacity-50' : ''
|
||||
} ${
|
||||
dragOverIndex === index && draggedIndex !== index
|
||||
? 'before:absolute before:left-0 before:right-0 before:top-0 before:h-0.5 before:bg-bambu-green'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<NavLink
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center ${sidebarExpanded ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition-colors ${
|
||||
`flex items-center ${sidebarExpanded ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition-colors group ${
|
||||
isActive
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'text-bambu-gray-light hover:bg-bambu-dark-tertiary hover:text-white'
|
||||
@@ -108,6 +211,9 @@ export function Layout() {
|
||||
}
|
||||
title={!sidebarExpanded ? label : undefined}
|
||||
>
|
||||
{sidebarExpanded && (
|
||||
<GripVertical className="w-4 h-4 flex-shrink-0 opacity-0 group-hover:opacity-50 cursor-grab active:cursor-grabbing -ml-1" />
|
||||
)}
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
{sidebarExpanded && <span>{label}</span>}
|
||||
</NavLink>
|
||||
@@ -196,7 +302,7 @@ export function Layout() {
|
||||
</main>
|
||||
|
||||
{/* Keyboard Shortcuts Modal */}
|
||||
{showShortcuts && <KeyboardShortcutsModal onClose={() => setShowShortcuts(false)} />}
|
||||
{showShortcuts && <KeyboardShortcutsModal onClose={() => setShowShortcuts(false)} navItems={navItems} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user