"use client";
import { useState, useCallback, useEffect } from "react";
import {
KeyRound,
RefreshCw,
Copy,
Check,
Eye,
EyeOff,
Shield,
ShieldCheck,
ShieldAlert,
} 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 { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
type CharacterOptions = {
uppercase: boolean;
lowercase: boolean;
numbers: boolean;
symbols: boolean;
};
type StrengthLevel = "weak" | "fair" | "good" | "strong" | "excellent";
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
const NUMBERS = "0123456789";
const SYMBOLS = "!@#$%^&*()_+-=[]{}|;:,.<>?";
export const title = "React Dialog Block Password Generator";
export default function DialogPasswordGenerator() {
const [open, setOpen] = useState(false);
const [password, setPassword] = useState("");
const [length, setLength] = useState(16);
const [options, setOptions] = useState<CharacterOptions>({
uppercase: true,
lowercase: true,
numbers: true,
symbols: true,
});
const [showPassword, setShowPassword] = useState(true);
const [copied, setCopied] = useState(false);
const generatePassword = useCallback(() => {
let charset = "";
if (options.uppercase) charset += UPPERCASE;
if (options.lowercase) charset += LOWERCASE;
if (options.numbers) charset += NUMBERS;
if (options.symbols) charset += SYMBOLS;
if (charset === "") {
charset = LOWERCASE;
}
let newPassword = "";
const array = new Uint32Array(length);
crypto.getRandomValues(array);
for (let i = 0; i < length; i++) {
newPassword += charset[array[i] % charset.length];
}
setPassword(newPassword);
}, [length, options]);
useEffect(() => {
if (open) {
generatePassword();
}
}, [open, generatePassword]);
const calculateStrength = (): { level: StrengthLevel; score: number } => {
let score = 0;
const activeOptions = Object.values(options).filter(Boolean).length;
// Length scoring
if (length >= 8) score += 20;
if (length >= 12) score += 20;
if (length >= 16) score += 20;
if (length >= 20) score += 10;
// Character variety scoring
score += activeOptions * 10;
// Determine level
if (score >= 80) return { level: "excellent", score };
if (score >= 60) return { level: "strong", score };
if (score >= 40) return { level: "good", score };
if (score >= 20) return { level: "fair", score };
return { level: "weak", score };
};
const strength = calculateStrength();
const getStrengthColor = (level: StrengthLevel) => {
switch (level) {
case "excellent":
return "text-primary";
case "strong":
return "text-primary";
case "good":
return "text-primary/80";
case "fair":
return "text-orange-500";
case "weak":
return "text-destructive";
}
};
const getStrengthIcon = (level: StrengthLevel) => {
switch (level) {
case "excellent":
case "strong":
return <ShieldCheck className="h-4 w-4" />;
case "good":
return <Shield className="h-4 w-4" />;
case "fair":
case "weak":
return <ShieldAlert className="h-4 w-4" />;
}
};
const handleCopy = () => {
navigator.clipboard.writeText(password);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
const handleOptionChange = (option: keyof CharacterOptions) => {
const newOptions = { ...options, [option]: !options[option] };
// Ensure at least one option is selected
if (Object.values(newOptions).some(Boolean)) {
setOptions(newOptions);
}
};
const handleClose = () => {
setOpen(false);
setTimeout(() => {
setCopied(false);
}, 200);
};
const activeOptionsCount = Object.values(options).filter(Boolean).length;
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<KeyRound className="mr-2 h-4 w-4" />
Generate Password
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<KeyRound className="h-5 w-5" />
Password Generator
</DialogTitle>
<DialogDescription>
Generate a secure password with custom options.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Generated Password</Label>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<div className="flex gap-2">
<div className="flex-1 rounded-md border bg-muted/50 px-3 py-2 font-mono text-sm overflow-hidden">
<span className={cn(!showPassword && "blur-sm select-none")}>
{password}
</span>
</div>
<Button
variant="outline"
size="icon"
onClick={generatePassword}
title="Regenerate"
>
<RefreshCw className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={handleCopy}
title="Copy"
>
{copied ? (
<Check className="h-4 w-4 text-primary" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Password Strength</Label>
<Badge
variant="outline"
className={cn("gap-1", getStrengthColor(strength.level))}
>
{getStrengthIcon(strength.level)}
<span className="capitalize">{strength.level}</span>
</Badge>
</div>
<Progress value={strength.score} className="h-2" />
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>Length: {length} characters</Label>
</div>
<Slider
value={[length]}
onValueChange={([value]) => setLength(value)}
min={8}
max={32}
step={1}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>8</span>
<span>16</span>
<span>24</span>
<span>32</span>
</div>
</div>
<div className="space-y-3">
<Label>Character Types</Label>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="uppercase" className="text-sm font-normal">
Uppercase (A-Z)
</Label>
</div>
<Switch
id="uppercase"
checked={options.uppercase}
onCheckedChange={() => handleOptionChange("uppercase")}
disabled={activeOptionsCount === 1 && options.uppercase}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="lowercase" className="text-sm font-normal">
Lowercase (a-z)
</Label>
</div>
<Switch
id="lowercase"
checked={options.lowercase}
onCheckedChange={() => handleOptionChange("lowercase")}
disabled={activeOptionsCount === 1 && options.lowercase}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="numbers" className="text-sm font-normal">
Numbers (0-9)
</Label>
</div>
<Switch
id="numbers"
checked={options.numbers}
onCheckedChange={() => handleOptionChange("numbers")}
disabled={activeOptionsCount === 1 && options.numbers}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="symbols" className="text-sm font-normal">
Symbols (!@#$%...)
</Label>
</div>
<Switch
id="symbols"
checked={options.symbols}
onCheckedChange={() => handleOptionChange("symbols")}
disabled={activeOptionsCount === 1 && options.symbols}
/>
</div>
</div>
</div>
{copied && (
<div className="flex items-center justify-center gap-2 text-sm text-primary">
<Check className="h-4 w-4" />
<span>Password copied to clipboard!</span>
</div>
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={handleClose}>
Close
</Button>
<Button onClick={handleCopy}>
<Copy className="mr-2 h-4 w-4" />
Copy Password
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}