SEO + AEO
This commit is contained in:
@@ -23,6 +23,7 @@ interface PasswordOptions {
|
||||
}
|
||||
|
||||
export function PasswordGenerator() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [showPassword, setShowPassword] = useState(true)
|
||||
const [copied, setCopied] = useState(false)
|
||||
@@ -35,8 +36,9 @@ export function PasswordGenerator() {
|
||||
excludeSimilar: false,
|
||||
})
|
||||
|
||||
// Load settings from localStorage on mount
|
||||
// Mount and load settings from localStorage
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
const savedOptions = localStorage.getItem('passmaster-settings')
|
||||
if (savedOptions) {
|
||||
try {
|
||||
@@ -50,8 +52,30 @@ export function PasswordGenerator() {
|
||||
|
||||
// Save settings to localStorage when options change
|
||||
useEffect(() => {
|
||||
localStorage.setItem('passmaster-settings', JSON.stringify(options))
|
||||
}, [options])
|
||||
if (mounted) {
|
||||
localStorage.setItem('passmaster-settings', JSON.stringify(options))
|
||||
}
|
||||
}, [options, mounted])
|
||||
|
||||
// Prevent hydration mismatch
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="card max-w-2xl mx-auto">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-2"></div>
|
||||
<div className="h-12 bg-gray-200 dark:bg-gray-700 rounded mb-4"></div>
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/3 mb-2"></div>
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded mb-4"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/3"></div>
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-2/3"></div>
|
||||
</div>
|
||||
<div className="h-12 bg-gray-200 dark:bg-gray-700 rounded mt-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleGenerate = () => {
|
||||
const newPassword = generatePassword(options)
|
||||
@@ -81,10 +105,10 @@ export function PasswordGenerator() {
|
||||
}
|
||||
|
||||
const getStrengthLevel = (entropy: number) => {
|
||||
if (entropy < 40) return { level: 'Weak', color: 'strength-weak', bg: 'bg-red-500' }
|
||||
if (entropy < 60) return { level: 'OK', color: 'strength-ok', bg: 'bg-yellow-500' }
|
||||
if (entropy < 80) return { level: 'Strong', color: 'strength-strong', bg: 'bg-blue-500' }
|
||||
return { level: 'Excellent', color: 'strength-excellent', bg: 'bg-green-500' }
|
||||
if (entropy < 40) return { level: 'Schwach', color: 'strength-weak', bg: 'bg-red-500' }
|
||||
if (entropy < 60) return { level: 'Mittel', color: 'strength-ok', bg: 'bg-yellow-500' }
|
||||
if (entropy < 80) return { level: 'Stark', color: 'strength-strong', bg: 'bg-blue-500' }
|
||||
return { level: 'Sehr Stark', color: 'strength-excellent', bg: 'bg-green-500' }
|
||||
}
|
||||
|
||||
const entropy = password ? calculateEntropy(password) : 0
|
||||
@@ -96,7 +120,7 @@ export function PasswordGenerator() {
|
||||
{/* Generated Password */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Generated Password
|
||||
Generiertes Passwort
|
||||
</label>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex-1 relative">
|
||||
@@ -105,13 +129,13 @@ export function PasswordGenerator() {
|
||||
value={password}
|
||||
readOnly
|
||||
className="input-field font-mono text-lg"
|
||||
placeholder="Click 'Generate Password' to create a secure password"
|
||||
aria-label="Generated password"
|
||||
placeholder="Klicken Sie auf 'Passwort generieren' für ein sicheres Passwort"
|
||||
aria-label="Generiertes Passwort"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-200"
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
aria-label={showPassword ? 'Passwort verstecken' : 'Passwort anzeigen'}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
||||
</button>
|
||||
@@ -122,7 +146,7 @@ export function PasswordGenerator() {
|
||||
className="px-4 py-3 bg-primary-600 text-white rounded-md hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2 transition-colors duration-200"
|
||||
whileHover={{ scale: password ? 1.05 : 1 }}
|
||||
whileTap={{ scale: password ? 0.95 : 1 }}
|
||||
aria-label="Copy password"
|
||||
aria-label="Passwort kopieren"
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{copied ? (
|
||||
@@ -147,7 +171,7 @@ export function PasswordGenerator() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<span>{copied ? 'Copied!' : 'Copy'}</span>
|
||||
<span>{copied ? 'Kopiert!' : 'Kopieren'}</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
@@ -155,7 +179,7 @@ export function PasswordGenerator() {
|
||||
{password && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">Strength:</span>
|
||||
<span className="text-gray-600 dark:text-gray-400">Stärke:</span>
|
||||
<span className={`font-medium ${strength.color.replace('strength-', 'text-')}`}>
|
||||
{strength.level}
|
||||
</span>
|
||||
@@ -170,7 +194,7 @@ export function PasswordGenerator() {
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>Entropy: {entropy.toFixed(1)} bits</span>
|
||||
<span>Time to crack: {timeToCrack}</span>
|
||||
<span>Zeit zum Knacken: {timeToCrack}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -181,7 +205,7 @@ export function PasswordGenerator() {
|
||||
{/* Length Slider */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Password Length: {options.length}
|
||||
Passwort-Länge: {options.length}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
@@ -200,12 +224,12 @@ export function PasswordGenerator() {
|
||||
{/* Character Options */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Character Types</h3>
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Zeichentypen</h3>
|
||||
{[
|
||||
{ key: 'includeUppercase', label: 'Uppercase (A-Z)' },
|
||||
{ key: 'includeLowercase', label: 'Lowercase (a-z)' },
|
||||
{ key: 'includeNumbers', label: 'Numbers (0-9)' },
|
||||
{ key: 'includeSymbols', label: 'Symbols (!@#$%^&*)' },
|
||||
{ key: 'includeUppercase', label: 'Großbuchstaben (A-Z)' },
|
||||
{ key: 'includeLowercase', label: 'Kleinbuchstaben (a-z)' },
|
||||
{ key: 'includeNumbers', label: 'Zahlen (0-9)' },
|
||||
{ key: 'includeSymbols', label: 'Symbole (!@#$%^&*)' },
|
||||
].map(({ key, label }) => (
|
||||
<label key={key} className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
@@ -220,7 +244,7 @@ export function PasswordGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Options</h3>
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Optionen</h3>
|
||||
<label className="flex items-start space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -229,11 +253,11 @@ export function PasswordGenerator() {
|
||||
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 mt-0.5"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Exclude Similar Characters</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Ähnliche Zeichen ausschließen</span>
|
||||
<div className="flex items-center space-x-1 mt-1">
|
||||
<Info className="h-3 w-3 text-gray-400" />
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Excludes 0/O, l/I, 1/I to avoid confusion
|
||||
Schließt 0/O, l/I, 1/I aus um Verwechslungen zu vermeiden
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,12 +274,12 @@ export function PasswordGenerator() {
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
<span>Generate Password</span>
|
||||
<span>Passwort generieren</span>
|
||||
</motion.button>
|
||||
|
||||
{/* ARIA Live Region for Copy Feedback */}
|
||||
<div aria-live="polite" className="sr-only">
|
||||
{copied && 'Password copied to clipboard'}
|
||||
{copied && 'Passwort in Zwischenablage kopiert'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user