"use client";
import { useState, useEffect, useCallback } from "react";
import {
Search,
Replace,
ChevronDown,
ChevronUp,
X,
Check,
CaseSensitive,
WholeWord,
Regex,
} 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 { Toggle } from "@/components/ui/toggle";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
type SearchOptions = {
caseSensitive: boolean;
wholeWord: boolean;
useRegex: boolean;
};
// Simulated document content for demo
const sampleContent = `The quick brown fox jumps over the lazy dog.
The Quick Brown Fox is a common pangram.
This fox is quick and the fox is brown.
Another line with fox mentioned twice: fox fox.`;
export const title = "React Dialog Block Search Replace";
export default function DialogSearchReplace() {
const [open, setOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [replaceTerm, setReplaceTerm] = useState("");
const [options, setOptions] = useState<SearchOptions>({
caseSensitive: false,
wholeWord: false,
useRegex: false,
});
const [currentMatch, setCurrentMatch] = useState(0);
const [totalMatches, setTotalMatches] = useState(0);
const [lastAction, setLastAction] = useState<string | null>(null);
const countMatches = useCallback(() => {
if (!searchTerm) {
setTotalMatches(0);
setCurrentMatch(0);
return;
}
try {
let pattern = searchTerm;
if (!options.useRegex) {
pattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
if (options.wholeWord) {
pattern = `\\b${pattern}\\b`;
}
const flags = options.caseSensitive ? "g" : "gi";
const regex = new RegExp(pattern, flags);
const matches = sampleContent.match(regex);
const count = matches ? matches.length : 0;
setTotalMatches(count);
if (count > 0 && currentMatch === 0) {
setCurrentMatch(1);
} else if (count === 0) {
setCurrentMatch(0);
} else if (currentMatch > count) {
setCurrentMatch(count);
}
} catch {
setTotalMatches(0);
setCurrentMatch(0);
}
}, [searchTerm, options, currentMatch]);
useEffect(() => {
countMatches();
}, [countMatches]);
const handleOptionToggle = (key: keyof SearchOptions) => {
setOptions((prev) => ({ ...prev, [key]: !prev[key] }));
};
const handleNext = () => {
if (totalMatches > 0) {
setCurrentMatch((prev) => (prev >= totalMatches ? 1 : prev + 1));
}
};
const handlePrevious = () => {
if (totalMatches > 0) {
setCurrentMatch((prev) => (prev <= 1 ? totalMatches : prev - 1));
}
};
const handleReplaceOne = () => {
if (totalMatches > 0) {
setLastAction(`Replaced 1 occurrence`);
setTotalMatches((prev) => Math.max(0, prev - 1));
if (currentMatch > totalMatches - 1) {
setCurrentMatch(Math.max(1, totalMatches - 1));
}
setTimeout(() => setLastAction(null), 2000);
}
};
const handleReplaceAll = () => {
if (totalMatches > 0) {
const count = totalMatches;
setLastAction(`Replaced ${count} occurrences`);
setTotalMatches(0);
setCurrentMatch(0);
setTimeout(() => setLastAction(null), 2000);
}
};
const handleClose = () => {
setOpen(false);
setTimeout(() => {
setSearchTerm("");
setReplaceTerm("");
setCurrentMatch(0);
setTotalMatches(0);
setLastAction(null);
}, 200);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<Search className="mr-2 h-4 w-4" />
Find and Replace
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Replace className="h-5 w-5" />
Find and Replace
</DialogTitle>
<DialogDescription>
Search for text and replace it across the document.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Search Input */}
<div className="space-y-2">
<Label htmlFor="search">Find</Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="search"
placeholder="Search text..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 pr-20"
/>
{searchTerm && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1">
<Badge variant="secondary" className="text-xs">
{totalMatches > 0
? `${currentMatch}/${totalMatches}`
: "0 results"}
</Badge>
</div>
)}
</div>
</div>
{/* Replace Input */}
<div className="space-y-2">
<Label htmlFor="replace">Replace with</Label>
<div className="relative">
<Replace className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="replace"
placeholder="Replace text..."
value={replaceTerm}
onChange={(e) => setReplaceTerm(e.target.value)}
className="pl-9"
/>
</div>
</div>
{/* Search Options */}
<div className="flex items-center gap-2">
<Toggle
pressed={options.caseSensitive}
onPressedChange={() => handleOptionToggle("caseSensitive")}
size="sm"
aria-label="Match case"
>
<CaseSensitive className="h-4 w-4" />
</Toggle>
<Toggle
pressed={options.wholeWord}
onPressedChange={() => handleOptionToggle("wholeWord")}
size="sm"
aria-label="Whole word"
>
<WholeWord className="h-4 w-4" />
</Toggle>
<Toggle
pressed={options.useRegex}
onPressedChange={() => handleOptionToggle("useRegex")}
size="sm"
aria-label="Use regex"
>
<Regex className="h-4 w-4" />
</Toggle>
<span className="text-xs text-muted-foreground ml-2">
{options.caseSensitive && "Case sensitive"}
{options.caseSensitive && options.wholeWord && " • "}
{options.wholeWord && "Whole word"}
{(options.caseSensitive || options.wholeWord) && options.useRegex && " • "}
{options.useRegex && "Regex"}
</span>
</div>
<Separator />
{/* Navigation */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={handlePrevious}
disabled={totalMatches === 0}
>
<ChevronUp className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={handleNext}
disabled={totalMatches === 0}
>
<ChevronDown className="h-4 w-4" />
</Button>
<span className="text-sm text-muted-foreground">
Navigate matches
</span>
</div>
</div>
{/* Action Feedback */}
{lastAction && (
<div className="flex items-center gap-2 rounded-lg border border-primary/50 p-3">
<Check className="h-4 w-4 text-primary" />
<span className="text-sm">{lastAction}</span>
</div>
)}
</div>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button variant="outline" onClick={handleClose} className="sm:mr-auto">
Close
</Button>
<Button
variant="outline"
onClick={handleReplaceOne}
disabled={totalMatches === 0 || !replaceTerm}
>
Replace
</Button>
<Button
onClick={handleReplaceAll}
disabled={totalMatches === 0 || !replaceTerm}
>
Replace All ({totalMatches})
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}