Skip to content

Commit c4a789a

Browse files
committed
feat: password strength meter on signup + fix stale eslint-disable comments
1 parent 301a451 commit c4a789a

3 files changed

Lines changed: 41 additions & 1 deletion

File tree

web/app/src/lib/auth.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
8080
)
8181
}
8282

83+
// eslint-disable-next-line react-refresh/only-export-components
8384
export function useAuth() {
8485
const ctx = useContext(AuthContext)
8586
if (!ctx) throw new Error('useAuth must be used within <AuthProvider>')

web/app/src/lib/useRoles.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export function useRoles() {
2626
}
2727
// Return cache if same user
2828
if (cachedUserId === user.id && cachedRoles) {
29-
// eslint-disable-next-line react-hooks/set-state-in-effect
3029
setRoles(cachedRoles)
3130
return
3231
}

web/app/src/routes/Onboarding.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function Onboarding() {
2020
const [acceptedPrivacy, setAcceptedPrivacy] = useState(false)
2121
const [submitting, setSubmitting] = useState(false)
2222
const [error, setError] = useState<string | null>(null)
23+
const [pwFocused, setPwFocused] = useState(false)
2324

2425
async function onSubmit(e: React.FormEvent) {
2526
e.preventDefault()
@@ -143,8 +144,13 @@ export function Onboarding() {
143144
placeholder="••••••••"
144145
value={password}
145146
onChange={(e) => setPassword(e.target.value)}
147+
onFocus={() => setPwFocused(true)}
148+
onBlur={() => setPwFocused(false)}
146149
className="w-full bg-transparent text-sm outline-none placeholder:text-[var(--color-stone-soft)]"
147150
/>
151+
{mode === 'signup' && (pwFocused || password.length > 0) ? (
152+
<PasswordStrength password={password} />
153+
) : null}
148154
</Field>
149155
</div>
150156

@@ -219,6 +225,40 @@ function Field({ label, hint, children }: { label: React.ReactNode; hint?: strin
219225
)
220226
}
221227

228+
function scorePassword(pw: string): { score: number; label: string; color: string } {
229+
let score = 0
230+
if (pw.length >= 8) score++
231+
if (pw.length >= 12) score++
232+
if (/[A-Z]/.test(pw)) score++
233+
if (/[0-9]/.test(pw)) score++
234+
if (/[^A-Za-z0-9]/.test(pw)) score++
235+
if (score <= 1) return { score, label: 'Weak', color: 'bg-[var(--color-coral)]' }
236+
if (score <= 2) return { score, label: 'Fair', color: 'bg-amber-400' }
237+
if (score <= 3) return { score, label: 'Good', color: 'bg-yellow-400' }
238+
if (score <= 4) return { score, label: 'Strong', color: 'bg-emerald-400' }
239+
return { score, label: 'Great', color: 'bg-emerald-500' }
240+
}
241+
242+
function PasswordStrength({ password }: { password: string }) {
243+
const { score, label, color } = scorePassword(password)
244+
const filled = Math.max(1, score)
245+
return (
246+
<div className="mt-3 space-y-1.5">
247+
<div className="flex gap-1">
248+
{Array.from({ length: 5 }).map((_, i) => (
249+
<div
250+
key={i}
251+
className={`h-1 flex-1 rounded-full transition-colors duration-300 ${
252+
i < filled ? color : 'bg-[var(--color-hairline)]'
253+
}`}
254+
/>
255+
))}
256+
</div>
257+
<p className="text-[10px] text-[var(--color-stone)]">{label}</p>
258+
</div>
259+
)
260+
}
261+
222262
function humanizeError(code: string): string {
223263
switch (code) {
224264
case 'email_taken':

0 commit comments

Comments
 (0)