From 164b7e65b6b0764470a30a78dbd1c2725edee39e Mon Sep 17 00:00:00 2001 From: greymantron Date: Mon, 30 Mar 2026 00:59:27 +0100 Subject: [PATCH] feat: add interactive password strength meter --- frontend/package.json | 4 +- frontend/src/components/RegistrationForm.tsx | 50 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 0d68018..d2bb125 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,7 +44,8 @@ "remark-gfm": "^4.0.0", "socket.io-client": "^4.8.1", "stellar-sdk": "^12.2.0", - "zustand": "^5.0.12" + "zustand": "^5.0.12", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@axe-core/cli": "^4.11.1", @@ -55,6 +56,7 @@ "@types/prismjs": "^1.26.5", "@types/react": "18.3.28", "@types/react-dom": "^18.3.7", + "@types/zxcvbn": "^4.4.5", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", diff --git a/frontend/src/components/RegistrationForm.tsx b/frontend/src/components/RegistrationForm.tsx index 6d2df3e..fa20e32 100644 --- a/frontend/src/components/RegistrationForm.tsx +++ b/frontend/src/components/RegistrationForm.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { registerMerchant, type Merchant } from "../lib/auth"; import MaskedValue from "./MaskedValue"; import toast from "react-hot-toast"; +import zxcvbn from "zxcvbn"; import { useSetMerchantApiKey, useSetMerchantMetadata, @@ -14,6 +15,7 @@ export default function RegistrationForm() { const setApiKey = useSetMerchantApiKey(); const setMerchant = useSetMerchantMetadata(); const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); const [businessName, setBusinessName] = useState(""); const [notificationEmail, setNotificationEmail] = useState(""); const [loading, setLoading] = useState(false); @@ -138,6 +140,54 @@ export default function RegistrationForm() { /> +
+ + setPassword(e.target.value)} + className="rounded-xl border border-white/10 bg-white/5 p-3 text-white placeholder:text-slate-600 focus:border-mint/50 focus:outline-none focus:ring-1 focus:ring-mint/50" + placeholder="••••••••" + /> + {/* Strength Meter */} +
+
+ {[0, 1, 2, 3].map((index) => { + const score = password ? zxcvbn(password).score : 0; + const activeBars = score === 0 ? 1 : score === 4 ? 4 : score + 1; + const isActive = password.length > 0 && index < activeBars; + let bgColor = "bg-white/10"; + + if (isActive) { + if (score === 0) bgColor = "bg-red-500"; + else if (score === 1) bgColor = "bg-orange-500"; + else if (score === 2) bgColor = "bg-yellow-400"; + else if (score === 3) bgColor = "bg-lime-400"; + else if (score === 4) bgColor = "bg-green-500"; + } + + return ( +
+ ); + })} +
+ {password.length > 0 && ( +

+ {["Weak", "Fair", "Good", "Strong", "Strong"][zxcvbn(password).score]} +

+ )} +
+
+