feat(App, DevicesPage): implement toast notifications for save status and update DevicesPage UI
This commit is contained in:
parent
3da4d8afc3
commit
e4c2aa0dbc
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Cpu, Settings, Wifi, Network, HardDrive, Activity, Search, Wrench, User, LogOut, Bell, FileText, AppWindow, Folder, Edit, Eye, Database, Upload, Download, Code2, LayoutList, Image, ChevronLeft, Loader2, Check, AlertCircle, RefreshCw, Terminal, Link, Printer, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { Toaster } from 'sonner';
|
||||
import { Cpu, Settings, Wifi, Network, HardDrive, Activity, Search, Wrench, User, LogOut, Bell, FileText, AppWindow, Folder, Edit, Eye, Database, Upload, Download, Code2, LayoutList, Image, ChevronLeft, Loader2, Terminal, Link, Printer, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { Toaster, toast } from 'sonner';
|
||||
import StatusPage from './components/StatusPage';
|
||||
import DevicesPage from './components/DevicesPage';
|
||||
import GeneralPage from './components/GeneralPage';
|
||||
|
|
@ -49,6 +49,28 @@ export default function App() {
|
|||
const [devicesOpenId, setDevicesOpenId] = useState<string | null>(null);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const id = 'save-status';
|
||||
switch (saveStatus) {
|
||||
case 'idle': toast.dismiss(id); break;
|
||||
case 'loading': toast.loading('Loading settings…', { id }); break;
|
||||
case 'unsaved':
|
||||
toast(`${pendingCount} unsaved change${pendingCount === 1 ? '' : 's'}`, {
|
||||
id, duration: Infinity,
|
||||
action: { label: 'Save', onClick: flushNow },
|
||||
});
|
||||
break;
|
||||
case 'saving': toast.loading('Saving…', { id }); break;
|
||||
case 'saved': toast.success('Settings saved', { id, duration: 2000 }); break;
|
||||
case 'error':
|
||||
toast.error('Save failed', {
|
||||
id, duration: Infinity,
|
||||
action: { label: 'Retry', onClick: reload },
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [saveStatus, pendingCount, flushNow, reload]);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = () => setIsFullscreen(!!document.fullscreenElement);
|
||||
document.addEventListener('fullscreenchange', onChange);
|
||||
|
|
@ -231,7 +253,6 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
|||
>
|
||||
<AppWindow className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
<SaveStatusBadge status={saveStatus} pendingCount={pendingCount} onSave={flushNow} onReload={reload} />
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
||||
|
|
@ -336,73 +357,3 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiny indicator that reflects the current save status of the settings
|
||||
* file. Renders nothing when idle (so it doesn't clutter the header).
|
||||
*/
|
||||
function SaveStatusBadge({
|
||||
status,
|
||||
pendingCount,
|
||||
onSave,
|
||||
onReload,
|
||||
}: {
|
||||
status: 'idle' | 'loading' | 'unsaved' | 'saving' | 'saved' | 'error';
|
||||
pendingCount: number;
|
||||
onSave: () => void;
|
||||
onReload: () => void;
|
||||
}) {
|
||||
if (status === 'idle') return null;
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<span className="text-xs text-white/80 inline-flex items-center gap-1" title="Loading settings from /.sys/config.json">
|
||||
<Loader2 className="w-3 h-3 animate-spin" /> Loading
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'unsaved') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="text-xs text-amber-300" title={`${pendingCount} unsaved change${pendingCount === 1 ? '' : 's'}`}>
|
||||
{pendingCount} unsaved
|
||||
</span>
|
||||
<button
|
||||
onClick={onSave}
|
||||
className="text-xs bg-amber-500 hover:bg-amber-400 text-white px-2 py-0.5 rounded"
|
||||
title="Save now"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'saving') {
|
||||
return (
|
||||
<span className="text-xs text-white/80 inline-flex items-center gap-1" title="Saving to /.sys/config.json">
|
||||
<Loader2 className="w-3 h-3 animate-spin" /> Saving
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'saved') {
|
||||
return (
|
||||
<span className="text-xs text-white/80 inline-flex items-center gap-1" title="Saved to /.sys/config.json">
|
||||
<Check className="w-3 h-3" /> Saved
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// error
|
||||
return (
|
||||
<button
|
||||
onClick={onReload}
|
||||
className="text-xs text-red-300 hover:text-white inline-flex items-center gap-1"
|
||||
title="Failed to save to /.sys/config.json — click to retry"
|
||||
>
|
||||
<AlertCircle className="w-3 h-3" /> Save failed
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -340,14 +340,14 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
|
|||
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${cassette.enabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{/* <div className="p-4">
|
||||
<label className="text-sm text-neutral-500 block mb-2">Play/Record</label>
|
||||
<input type="text" value={cassette.play_record || ''} onChange={(e) => updateSetting(['cassette', 'play_record'], e.target.value)} className="w-full px-3 py-2 border border-neutral-300 rounded-lg" />
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<label className="text-sm text-neutral-500 block mb-2">Pulldown</label>
|
||||
<input type="text" value={cassette.pulldown || ''} onChange={(e) => updateSetting(['cassette', 'pulldown'], e.target.value)} className="w-full px-3 py-2 border border-neutral-300 rounded-lg" />
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="p-4">
|
||||
<label className="text-sm text-neutral-500 block mb-2">URL</label>
|
||||
<input type="text" value={cassette.url || ''} onChange={(e) => updateSetting(['cassette', 'url'], e.target.value)} className="w-full px-3 py-2 border border-neutral-300 rounded-lg" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user