Sidebar items can now be reordered; Settinsg for default view added

This commit is contained in:
Martin Ziegler
2025-11-30 09:45:57 +01:00
parent ba302394d5
commit 2cc7539738
8 changed files with 38084 additions and 450 deletions
@@ -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()}>
+146 -40
View File
@@ -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>
);
}
+56 -1
View File
@@ -1,11 +1,12 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Save, Loader2, Check, Plus, Plug, AlertTriangle } from 'lucide-react';
import { Save, Loader2, Check, Plus, Plug, AlertTriangle, RotateCcw } from 'lucide-react';
import { api } from '../api/client';
import type { AppSettings, SmartPlug } from '../api/client';
import { Card, CardContent, CardHeader } from '../components/Card';
import { Button } from '../components/Button';
import { SmartPlugCard } from '../components/SmartPlugCard';
import { AddSmartPlugModal } from '../components/AddSmartPlugModal';
import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout';
import { useState, useEffect } from 'react';
export function SettingsPage() {
@@ -15,6 +16,17 @@ export function SettingsPage() {
const [showSaved, setShowSaved] = useState(false);
const [showPlugModal, setShowPlugModal] = useState(false);
const [editingPlug, setEditingPlug] = useState<SmartPlug | null>(null);
const [defaultView, setDefaultViewState] = useState<string>(getDefaultView());
const handleDefaultViewChange = (path: string) => {
setDefaultViewState(path);
setDefaultView(path);
};
const handleResetSidebarOrder = () => {
localStorage.removeItem('sidebarOrder');
window.location.reload();
};
const { data: settings, isLoading } = useQuery({
queryKey: ['settings'],
@@ -244,6 +256,49 @@ export function SettingsPage() {
</CardContent>
</Card>
<Card>
<CardHeader>
<h2 className="text-lg font-semibold text-white">Interface</h2>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="block text-sm text-bambu-gray mb-1">
Default view on startup
</label>
<select
value={defaultView}
onChange={(e) => handleDefaultViewChange(e.target.value)}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
>
{defaultNavItems.map((item) => (
<option key={item.id} value={item.to}>
{item.label}
</option>
))}
</select>
<p className="text-xs text-bambu-gray mt-1">
Page to show when opening the app
</p>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-white">Sidebar order</p>
<p className="text-sm text-bambu-gray">
Drag items in the sidebar to reorder. Reset to default order here.
</p>
</div>
<Button
variant="secondary"
size="sm"
onClick={handleResetSidebarOrder}
>
<RotateCcw className="w-4 h-4" />
Reset
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h2 className="text-lg font-semibold text-white">About</h2>