feat: add toast notifications for file actions
All checks were successful
Automated Container Build / build-and-push (push) Successful in 39s

This commit is contained in:
Elijah 2026-05-23 10:47:36 -07:00
parent cf43dd0e5a
commit ee36e16dae

View file

@ -7,7 +7,8 @@ import {
ChevronRight, Grid3X3, List, Plus, Upload, LogOut,
MoreVertical, Star, Download, Pencil, Copy, Move,
FolderPlus, ArrowUp, X, Check, RefreshCw, Info, Link as LinkIcon, Image as ImageIcon, Film as FilmIcon,
Sun, Moon, Music as MusicIcon, Code as CodeIcon, Archive as ArchiveIcon, CornerDownRight
Sun, Moon, Music as MusicIcon, Code as CodeIcon, Archive as ArchiveIcon, CornerDownRight,
CheckCircle, AlertCircle
} from 'lucide-react';
import api from '@/lib/api';
@ -105,6 +106,12 @@ interface UploadProgress {
error?: string;
}
interface Toast {
id: number;
message: string;
type?: 'success' | 'error' | 'info';
}
function getFileDisplayName(name: string, isDir: boolean) {
if (isDir) return name;
const dotIndex = name.lastIndexOf('.');
@ -236,6 +243,15 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const [sortAsc, setSortAsc] = useState(true);
const [draggedFile, setDraggedFile] = useState<string | null>(null);
const [uploads, setUploads] = useState<UploadProgress[]>([]);
const [toasts, setToasts] = useState<Toast[]>([]);
function showToast(message: string, type: 'success' | 'error' | 'info' = 'success') {
const id = Date.now() + Math.random();
setToasts(prev => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 3000);
}
const [showTrashModal, setShowTrashModal] = useState(false);
const [showMoveModal, setShowMoveModal] = useState(false);
const [movingFiles, setMovingFiles] = useState<string[]>([]);
@ -599,17 +615,21 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
async function deleteSelected() {
if (selectedFiles.size === 0) return;
if (!confirm(`Move ${selectedFiles.size} items to trash?`)) return;
const count = selectedFiles.size;
for (const path of selectedFiles) {
await api.deleteFile(path);
}
setSelectedFiles(new Set());
loadFiles();
loadFiles(undefined, true);
showToast(`Deleted ${count} item${count > 1 ? 's' : ''}`);
}
async function handlePin(file: FileItem) {
await api.togglePin(file.path);
loadFiles(undefined, true);
setPinnedRefreshCounter(c => c + 1);
showToast(file.is_pinned ? 'Unpinned item' : 'Pinned item');
}
async function handleRename(path: string, originalName: string, isDir: boolean) {
@ -629,6 +649,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
setRenaming(null);
setNewName('');
loadFiles(undefined, true);
showToast(`Renamed to ${finalName}`);
}
async function handleCreateFolder() {
@ -643,11 +664,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
async function handleRestore(file: FileItem) {
await api.restoreFromTrash(file.path);
loadTrash();
showToast(`Restored ${file.name}`);
}
async function handleEmptyTrash() {
await api.emptyTrash();
loadTrash();
showToast('Trash emptied');
}
async function handleMoveExecute() {
@ -658,6 +681,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
setMovingFiles([]);
loadFiles(undefined, true);
setSelectedFiles(new Set());
showToast(`Moved ${movingFiles.length} item${movingFiles.length > 1 ? 's' : ''}`);
} catch (err) {
console.error(err);
}
@ -706,6 +730,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
try {
await api.move(sourcePath, file.path);
loadFiles(undefined, true);
showToast('Item moved');
} catch (err) {
console.error(err);
}
@ -1553,15 +1578,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<button
className="p-2 hover:bg-red-500/10 rounded-lg transition-colors flex items-center gap-2 text-sm text-red-500"
title="Delete Selected"
onClick={async () => {
if (confirm(`Move ${selectedFiles.size} items to trash?`)) {
for (const path of Array.from(selectedFiles)) {
await api.deleteFile(path).catch(() => {});
}
setSelectedFiles(new Set());
loadFiles(undefined, true);
}
}}
onClick={() => deleteSelected()}
>
<Trash2 className="w-4 h-4" />
</button>
@ -1676,8 +1693,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
label="Move to Trash"
danger
onClick={() => {
if (confirm(`Move ${contextMenu.file.name} to trash?`)) {
api.deleteFile(contextMenu.file.path).then(() => {
loadFiles(undefined, true);
showToast(`Deleted ${contextMenu.file.name}`);
});
}
setContextMenu(null);
api.deleteFile(contextMenu.file.path).then(() => loadFiles(undefined, true));
}}
/>
</>
@ -1716,6 +1738,30 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
)}
</AnimatePresence>
{/* Toast Notifications */}
<div className="fixed bottom-6 right-6 z-[60] flex flex-col gap-2 pointer-events-none">
<AnimatePresence>
{toasts.map(toast => (
<motion.div
key={toast.id}
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.95 }}
className="px-4 py-3 rounded-xl shadow-xl flex items-center gap-3 backdrop-blur-md border border-white/10 pointer-events-auto"
style={{
backgroundColor: toast.type === 'error' ? 'rgba(239, 68, 68, 0.9)' : 'var(--color-bg-elevated)',
color: toast.type === 'error' ? 'white' : 'var(--color-text-primary)'
}}
>
{toast.type === 'success' && <CheckCircle className="w-5 h-5 text-green-500" />}
{toast.type === 'error' && <AlertCircle className="w-5 h-5 text-white" />}
{toast.type === 'info' && <Info className="w-5 h-5 text-blue-500" />}
<span className="font-medium text-sm">{toast.message}</span>
</motion.div>
))}
</AnimatePresence>
</div>
{/* Upload Progress Panel */}
<AnimatePresence>
{uploads.length > 0 && (