"use client";
import { useState, useEffect, useRef } from "react";
import {
FileText,
File,
AlertTriangle,
Check,
X,
Info,
FileWarning,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { cn } from "@/lib/utils";
type ValidationResult = {
valid: boolean;
message?: string;
type?: "error" | "warning";
};
const existingFiles = [
"document.pdf",
"report.pdf",
"image.png",
"photo.jpg",
"data.xlsx",
"notes.txt",
"backup.zip",
];
const invalidChars = /[\\/:*?"<>|]/;
const reservedNames = ["con", "prn", "aux", "nul", "com1", "lpt1"];
export const title = "React Dialog Block File Rename";
export default function DialogFileRename() {
const [open, setOpen] = useState(false);
const [originalFile] = useState({ name: "document", extension: "pdf" });
const [newName, setNewName] = useState("document");
const [preserveExtension, setPreserveExtension] = useState(true);
const [customExtension, setCustomExtension] = useState("pdf");
const [validation, setValidation] = useState<ValidationResult>({ valid: true });
const [hasConflict, setHasConflict] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (open) {
setNewName(originalFile.name);
setCustomExtension(originalFile.extension);
setTimeout(() => {
inputRef.current?.select();
}, 100);
}
}, [open, originalFile]);
useEffect(() => {
validateName(newName, preserveExtension ? originalFile.extension : customExtension);
}, [newName, preserveExtension, customExtension, originalFile.extension]);
const validateName = (name: string, ext: string) => {
// Empty check
if (!name.trim()) {
setValidation({ valid: false, message: "Filename cannot be empty", type: "error" });
setHasConflict(false);
return;
}
// Invalid characters
if (invalidChars.test(name)) {
setValidation({
valid: false,
message: "Filename contains invalid characters: \\ / : * ? \" < > |",
type: "error",
});
setHasConflict(false);
return;
}
// Reserved names
if (reservedNames.includes(name.toLowerCase())) {
setValidation({
valid: false,
message: "This is a reserved system name",
type: "error",
});
setHasConflict(false);
return;
}
// Starts with dot or space
if (name.startsWith(".") || name.startsWith(" ")) {
setValidation({
valid: false,
message: "Filename cannot start with a dot or space",
type: "error",
});
setHasConflict(false);
return;
}
// Ends with space or dot
if (name.endsWith(" ") || name.endsWith(".")) {
setValidation({
valid: false,
message: "Filename cannot end with a space or dot",
type: "error",
});
setHasConflict(false);
return;
}
// Check for conflicts
const fullName = `${name}.${ext}`;
const originalFullName = `${originalFile.name}.${originalFile.extension}`;
if (fullName !== originalFullName && existingFiles.includes(fullName.toLowerCase())) {
setValidation({
valid: false,
message: `A file named "${fullName}" already exists`,
type: "error",
});
setHasConflict(true);
return;
}
// Extension change warning
if (!preserveExtension && ext !== originalFile.extension) {
setValidation({
valid: true,
message: "Changing the extension may make the file unusable",
type: "warning",
});
setHasConflict(false);
return;
}
// Same name check
if (name === originalFile.name && ext === originalFile.extension) {
setValidation({
valid: false,
message: "The new name is the same as the original",
type: "error",
});
setHasConflict(false);
return;
}
setValidation({ valid: true });
setHasConflict(false);
};
const handleRename = () => {
if (!validation.valid) return;
const ext = preserveExtension ? originalFile.extension : customExtension;
console.log("Renamed:", `${originalFile.name}.${originalFile.extension}`, "→", `${newName}.${ext}`);
setOpen(false);
};
const handleClose = () => {
setOpen(false);
setTimeout(() => {
setNewName(originalFile.name);
setPreserveExtension(true);
setCustomExtension(originalFile.extension);
}, 200);
};
const getFullFileName = () => {
const ext = preserveExtension ? originalFile.extension : customExtension;
return `${newName}.${ext}`;
};
const hasChanges = () => {
const ext = preserveExtension ? originalFile.extension : customExtension;
return newName !== originalFile.name || ext !== originalFile.extension;
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<FileText className="mr-2 h-4 w-4" />
Rename File
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Rename File
</DialogTitle>
<DialogDescription>
Enter a new name for this file.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="flex items-center gap-3 rounded-lg border p-3">
<File className="h-10 w-10 text-muted-foreground shrink-0" />
<div className="min-w-0 flex-1">
<p className="font-medium truncate">
{originalFile.name}.{originalFile.extension}
</p>
<p className="text-sm text-muted-foreground">Original filename</p>
</div>
<Badge variant="secondary" className="uppercase shrink-0">
{originalFile.extension}
</Badge>
</div>
<div className="space-y-2">
<Label htmlFor="filename">New filename</Label>
<div className="flex gap-2">
<Input
ref={inputRef}
id="filename"
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="Enter filename"
className={cn(
"flex-1",
validation.type === "error" && "border-destructive focus-visible:ring-destructive"
)}
/>
<div className="flex items-center rounded-md border bg-muted px-3 text-sm text-muted-foreground">
.{preserveExtension ? originalFile.extension : customExtension}
</div>
</div>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="preserve-ext" className="text-sm font-normal">
Preserve extension
</Label>
<p className="text-xs text-muted-foreground">
Keep the original file extension
</p>
</div>
<Switch
id="preserve-ext"
checked={preserveExtension}
onCheckedChange={setPreserveExtension}
/>
</div>
{!preserveExtension && (
<div className="space-y-2">
<Label htmlFor="extension">New extension</Label>
<Input
id="extension"
value={customExtension}
onChange={(e) => setCustomExtension(e.target.value.replace(/\./g, ""))}
placeholder="pdf"
className="max-w-[120px]"
/>
</div>
)}
{validation.message && (
<Alert variant={validation.type === "error" ? "destructive" : "default"}>
{validation.type === "error" ? (
hasConflict ? (
<FileWarning className="h-4 w-4" />
) : (
<X className="h-4 w-4" />
)
) : (
<AlertTriangle className="h-4 w-4" />
)}
<AlertDescription>{validation.message}</AlertDescription>
</Alert>
)}
{hasChanges() && validation.valid && (
<div className="rounded-lg border p-3 space-y-2">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Info className="h-3 w-3" />
<span>Preview</span>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground line-through">
{originalFile.name}.{originalFile.extension}
</span>
<span className="text-muted-foreground">→</span>
<span className="font-medium">{getFullFileName()}</span>
</div>
</div>
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleRename} disabled={!validation.valid || !hasChanges()}>
<Check className="mr-2 h-4 w-4" />
Rename
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}