Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/components/DeleteConfirmModal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.dcm-backdrop {
position: fixed; inset: 0;
background: rgba(0, 0, 0, 0.55);
display: flex; align-items: center; justify-content: center;
z-index: 9999;
}
.dcm-box {
background: #1e2327; border: 1px solid #333;
border-radius: 10px; padding: 28px; width: 360px; max-width: 90vw;
}
.dcm-title {
color: #e0e0e0; font-size: 16px; font-weight: 600;
text-align: center; margin: 0 0 12px;
}
.dcm-body {
color: #aaa; font-size: 13.5px; text-align: center;
line-height: 1.6; margin: 0 0 22px;
}
.dcm-body strong { color: #e0e0e0; }
.dcm-note { display: block; margin-top: 8px; color: #e5a820; font-size: 12px; }
.dcm-actions { display: flex; gap: 10px; }
.dcm-cancel {
flex: 1; padding: 8px 0; background: #2e3338;
color: #ccc; border: none; border-radius: 6px; cursor: pointer;
}
.dcm-confirm {
flex: 1; padding: 8px 0; background: #c0392b;
color: #fff; border: none; border-radius: 6px;
font-weight: 500; cursor: pointer;
}
39 changes: 39 additions & 0 deletions src/components/DeleteConfirmModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect, useRef } from "react";
import "./DeleteConfirmModal.css";

export default function DeleteConfirmModal({ target, onCancel, onConfirm }) {
const confirmRef = useRef(null);

useEffect(() => {
confirmRef.current?.focus();
const handler = (e) => {
if (e.key === "Escape") onCancel();
if (e.key === "Enter" && !confirmRef.current?.contains(document.activeElement)) onConfirm();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
Comment thread
nidhii-dev marked this conversation as resolved.
}, [onCancel, onConfirm]);
}, [onCancel, onConfirm]);

const isFolder = target?.type === "folder";

return (
<div className="dcm-backdrop" onClick={onCancel}>
<div className="dcm-box" onClick={(e) => e.stopPropagation()}>
<h2 className="dcm-title">Delete {isFolder ? "folder" : "file"}?</h2>
<p className="dcm-body">
<strong>{target?.name}</strong> will be permanently deleted
{isFolder && target.childCount > 0
? ` along with ${target.childCount} item${target.childCount === 1 ? '' : 's'} directly inside`
: ""}.
<br />
<span className="dcm-note">⚠️ This affects everyone in the room.</span>
</p>
<div className="dcm-actions">
<button className="dcm-cancel" onClick={onCancel}>Cancel</button>
<button className="dcm-confirm" onClick={onConfirm} ref={confirmRef}>Delete</button>
</div>
</div>
</div>
);
}
61 changes: 59 additions & 2 deletions src/components/FileExplorer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { v4 as uuid } from 'uuid';
import {
File,
Expand All @@ -12,6 +12,7 @@ import {
ChevronRight,
ChevronDown,
} from 'lucide-react';
import DeleteConfirmModal from "./DeleteConfirmModal";

// File icon by extension — returns a Lucide component
function FileIcon({ name }) {
Expand All @@ -28,6 +29,8 @@ function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreate
const [renaming, setRenaming] = useState(false);
const [renameVal, setRenameVal] = useState(node.name);
const [showCreate, setShowCreate] = useState(null); // 'file' | 'folder'
const [deleteTarget, setDeleteTarget] = useState(null);
const [undoToast, setUndoToast] = useState(null);
const [createName, setCreateName] = useState('');
const renameRef = useRef(null);
const createRef = useRef(null);
Expand Down Expand Up @@ -60,6 +63,43 @@ function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreate
setExpanded(true);
};

const handleDeleteClick = (nodeId, name, type, childCount = 0) => {
setDeleteTarget({ nodeId, name, type, childCount });
};

const handleDeleteConfirm = () => {
const { nodeId, name } = deleteTarget;
setDeleteTarget(null);

const timer = setTimeout(() => {
onDeleteNode(nodeId);
setUndoToast(null);
}, 5000);
Comment on lines +74 to +77
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAJOR RELIABILITY Pending delete timer is not cleared on FileNode unmount

The setTimeout stored in undoToast.timer is never cancelled if the FileNode unmounts during the 5-second window (e.g. parent re-renders, room closes). When it fires, onDeleteNode(nodeId) executes unintentionally and setUndoToast(null) is called on an unmounted component.

Prompt to fix with AI

Copy this prompt into your AI coding assistant to fix this issue.

In `src/components/FileExplorer.js`, the `FileNode` component creates a `setTimeout` inside `handleDeleteConfirm` (lines 74-77) and stores it in `undoToast.timer`, but never clears it when the component unmounts. Add a `useEffect` cleanup:

1. Add `useEffect` to the import on line 1: `import React, { useState, useRef, useEffect } from 'react';`
2. After the `handleUndo` function (after line 85), add:
```js
useEffect(() => {
    return () => {
        if (undoToast?.timer) clearTimeout(undoToast.timer);
    };
}, [undoToast]);

This ensures the pending deletion timer is cancelled if the component unmounts during the 5-second undo window, preventing unintended file deletion.


</details>


setUndoToast({ label: name, timer });
};

const handleUndo = () => {
clearTimeout(undoToast.timer);
setUndoToast(null);
};

useEffect(() => {
return () => {
if (undoToast?.timer) {
clearTimeout(undoToast.timer);
}
};
}, [undoToast]);

useEffect(() => {
return () => {
if (undoToast?.timer) {
clearTimeout(undoToast.timer);
}
};
}, [undoToast]);

const children = (node.children || []).map(id => fileSystem[id]).filter(Boolean);

return (
Expand Down Expand Up @@ -115,7 +155,7 @@ function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreate
<button title="Rename" onClick={() => { setRenaming(true); setRenameVal(node.name); }}>
<Pencil size={13} />
</button>
<button title="Delete" onClick={() => onDeleteNode(node.id)}>
<button title="Delete" onClick={() => handleDeleteClick(node.id, node.name, node.type, node.children?.length ?? 0)}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT CORRECTNESS childCount is shallow-only but modal says "all X files inside"

FileExplorer.js passes node.children?.length (direct children only) as childCount, but DeleteConfirmModal.js:26 displays it as "along with all X files inside" — language that implies a deep/recursive count. A folder with 2 subfolders each containing 10 files would show "2 files inside" instead of 22.

Prompt to fix with AI

Copy this prompt into your AI coding assistant to fix this issue.

Either (a) compute a recursive child count before passing it to handleDeleteClick — add a helper like `function countDescendants(node, fs) { return (node.children || []).reduce((n, id) => n + 1 + countDescendants(fs[id] || {}, fs), 0); }` and pass the result instead of `node.children?.length`, OR (b) change the modal copy to say "along with all items directly inside" to match the shallow count that is actually passed.

<Trash2 size={13} />
</button>
</>
Expand Down Expand Up @@ -161,6 +201,23 @@ function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreate
canWrite={canWrite}
/>
))}

{/* Delete Confirmation Modal */}
{deleteTarget && (
<DeleteConfirmModal
target={deleteTarget}
onCancel={() => setDeleteTarget(null)}
onConfirm={handleDeleteConfirm}
/>
)}

{/* Undo Toast */}
{undoToast && (
<div className="undo-toast">
<span>"{undoToast.label}" deleting in 5s…</span>
<button onClick={handleUndo}>Undo</button>
</div>
)}
</div>
);
}
Expand Down