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
+37455
View File
File diff suppressed because it is too large Load Diff
@@ -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>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
<script type="module" crossorigin src="/assets/index-BS0vyekL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CJ8fGWbx.css">
<script type="module" crossorigin src="/assets/index-BAoesX1A.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-IlXYH9s2.css">
</head>
<body>
<div id="root"></div>