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 { 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 { 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 } from 'sonner';
|
import { Toaster, toast } from 'sonner';
|
||||||
import StatusPage from './components/StatusPage';
|
import StatusPage from './components/StatusPage';
|
||||||
import DevicesPage from './components/DevicesPage';
|
import DevicesPage from './components/DevicesPage';
|
||||||
import GeneralPage from './components/GeneralPage';
|
import GeneralPage from './components/GeneralPage';
|
||||||
|
|
@ -49,6 +49,28 @@ export default function App() {
|
||||||
const [devicesOpenId, setDevicesOpenId] = useState<string | null>(null);
|
const [devicesOpenId, setDevicesOpenId] = useState<string | null>(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
const onChange = () => setIsFullscreen(!!document.fullscreenElement);
|
const onChange = () => setIsFullscreen(!!document.fullscreenElement);
|
||||||
document.addEventListener('fullscreenchange', onChange);
|
document.addEventListener('fullscreenchange', onChange);
|
||||||
|
|
@ -231,7 +253,6 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
||||||
>
|
>
|
||||||
<AppWindow className="w-5 h-5 text-white" />
|
<AppWindow className="w-5 h-5 text-white" />
|
||||||
</button>
|
</button>
|
||||||
<SaveStatusBadge status={saveStatus} pendingCount={pendingCount} onSave={flushNow} onReload={reload} />
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
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'}`} />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
{/* <div className="p-4">
|
||||||
<label className="text-sm text-neutral-500 block mb-2">Play/Record</label>
|
<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" />
|
<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>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<label className="text-sm text-neutral-500 block mb-2">Pulldown</label>
|
<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" />
|
<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">
|
<div className="p-4">
|
||||||
<label className="text-sm text-neutral-500 block mb-2">URL</label>
|
<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" />
|
<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