mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Sidebar items can now be reordered; Settinsg for default view added
This commit is contained in:
+37455
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()}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user